dato_json_schema 0.20.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +75 -0
- data/bin/validate-schema +40 -0
- data/lib/commands/validate_schema.rb +130 -0
- data/lib/json_pointer.rb +7 -0
- data/lib/json_pointer/evaluator.rb +80 -0
- data/lib/json_reference.rb +58 -0
- data/lib/json_schema.rb +31 -0
- data/lib/json_schema/attributes.rb +117 -0
- data/lib/json_schema/configuration.rb +28 -0
- data/lib/json_schema/document_store.rb +30 -0
- data/lib/json_schema/error.rb +85 -0
- data/lib/json_schema/parser.rb +390 -0
- data/lib/json_schema/reference_expander.rb +364 -0
- data/lib/json_schema/schema.rb +295 -0
- data/lib/json_schema/validator.rb +606 -0
- data/schemas/hyper-schema.json +168 -0
- data/schemas/schema.json +150 -0
- data/test/bin_test.rb +19 -0
- data/test/commands/validate_schema_test.rb +121 -0
- data/test/data_scaffold.rb +241 -0
- data/test/json_pointer/evaluator_test.rb +69 -0
- data/test/json_reference/reference_test.rb +45 -0
- data/test/json_schema/attribute_test.rb +121 -0
- data/test/json_schema/document_store_test.rb +42 -0
- data/test/json_schema/error_test.rb +18 -0
- data/test/json_schema/parser_test.rb +362 -0
- data/test/json_schema/reference_expander_test.rb +618 -0
- data/test/json_schema/schema_test.rb +46 -0
- data/test/json_schema/validator_test.rb +1078 -0
- data/test/json_schema_test.rb +46 -0
- data/test/test_helper.rb +17 -0
- metadata +77 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module JsonSchema
|
2
|
+
# Attributes mixes in some useful attribute-related methods for use in
|
3
|
+
# defining schema classes in a spirit similar to Ruby's attr_accessor and
|
4
|
+
# friends.
|
5
|
+
module Attributes
|
6
|
+
# Provides class-level methods for the Attributes module.
|
7
|
+
module ClassMethods
|
8
|
+
# Attributes that should be copied between classes when invoking
|
9
|
+
# Attributes#copy_from.
|
10
|
+
#
|
11
|
+
# Hash contains instance variable names mapped to a default value for the
|
12
|
+
# field.
|
13
|
+
attr_reader :copyable_attrs
|
14
|
+
|
15
|
+
# Attributes that are part of the JSON schema and hyper-schema
|
16
|
+
# specifications. These are allowed to be accessed with the [] operator.
|
17
|
+
#
|
18
|
+
# Hash contains the access key mapped to the name of the method that should
|
19
|
+
# be invoked to retrieve a value. For example, `type` maps to `type` and
|
20
|
+
# `additionalItems` maps to `additional_items`.
|
21
|
+
attr_reader :schema_attrs
|
22
|
+
|
23
|
+
# identical to attr_accessible, but allows us to copy in values from a
|
24
|
+
# target schema to help preserve our hierarchy during reference expansion
|
25
|
+
def attr_copyable(attr, options = {})
|
26
|
+
attr_accessor(attr)
|
27
|
+
|
28
|
+
ref = :"@#{attr}"
|
29
|
+
# Usually the default being assigned here is nil.
|
30
|
+
self.copyable_attrs[ref] = options[:default]
|
31
|
+
|
32
|
+
if default = options[:default]
|
33
|
+
# remove the reader already created by attr_accessor
|
34
|
+
remove_method(attr)
|
35
|
+
|
36
|
+
if [Array, Hash, Set].include?(default.class)
|
37
|
+
default = default.freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
define_method(attr) do
|
41
|
+
val = instance_variable_get(ref)
|
42
|
+
if !val.nil?
|
43
|
+
val
|
44
|
+
else
|
45
|
+
default
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if options[:clear_cache]
|
51
|
+
remove_method(:"#{attr}=")
|
52
|
+
define_method(:"#{attr}=") do |value|
|
53
|
+
instance_variable_set(options[:clear_cache], nil)
|
54
|
+
instance_variable_set(ref, value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def attr_schema(attr, options = {})
|
60
|
+
attr_copyable(attr, :default => options[:default], :clear_cache => options[:clear_cache])
|
61
|
+
self.schema_attrs[options[:schema_name] || attr] = attr
|
62
|
+
end
|
63
|
+
|
64
|
+
# Directive indicating that attributes should be inherited from a parent
|
65
|
+
# class.
|
66
|
+
#
|
67
|
+
# Must appear as first statement in class that mixes in (or whose parent
|
68
|
+
# mixes in) the Attributes module.
|
69
|
+
def inherit_attrs
|
70
|
+
@copyable_attrs = self.superclass.instance_variable_get(:@copyable_attrs).dup
|
71
|
+
@schema_attrs = self.superclass.instance_variable_get(:@schema_attrs).dup
|
72
|
+
end
|
73
|
+
|
74
|
+
# Initializes some class instance variables required to make other
|
75
|
+
# methods in the Attributes module work. Run automatically when the
|
76
|
+
# module is mixed into another class.
|
77
|
+
def initialize_attrs
|
78
|
+
@copyable_attrs = {}
|
79
|
+
@schema_attrs = {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.included(klass)
|
84
|
+
klass.extend(ClassMethods)
|
85
|
+
klass.send(:initialize_attrs)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Allows the values of schema attributes to be accessed with a symbol or a
|
89
|
+
# string. So for example, the value of `schema.additional_items` could be
|
90
|
+
# procured with `schema[:additionalItems]`. This only works for attributes
|
91
|
+
# that are part of the JSON schema specification; other methods on the
|
92
|
+
# class are not available (e.g. `expanded`.)
|
93
|
+
#
|
94
|
+
# This is implemented so that `JsonPointer::Evaluator` can evaluate a
|
95
|
+
# reference on an sintance of this class (as well as plain JSON data).
|
96
|
+
def [](name)
|
97
|
+
name = name.to_sym
|
98
|
+
if self.class.schema_attrs.key?(name)
|
99
|
+
send(self.class.schema_attrs[name])
|
100
|
+
else
|
101
|
+
raise NoMethodError, "Schema does not respond to ##{name}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def copy_from(schema)
|
106
|
+
self.class.copyable_attrs.each do |copyable, _|
|
107
|
+
instance_variable_set(copyable, schema.instance_variable_get(copyable))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize_attrs
|
112
|
+
self.class.copyable_attrs.each do |attr, _|
|
113
|
+
instance_variable_set(attr, nil)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JsonSchema
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :all_of_sub_errors
|
4
|
+
attr_reader :custom_formats
|
5
|
+
attr_reader :validate_regex_with
|
6
|
+
|
7
|
+
def validate_regex_with=(validator)
|
8
|
+
@validate_regex_with = validator
|
9
|
+
end
|
10
|
+
|
11
|
+
def register_format(name, validator_proc)
|
12
|
+
@custom_formats[name] = validator_proc
|
13
|
+
end
|
14
|
+
|
15
|
+
# Used for testing.
|
16
|
+
def reset!
|
17
|
+
@validate_regex_with = nil
|
18
|
+
@custom_formats = {}
|
19
|
+
@all_of_sub_errors = false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
reset!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JsonSchema
|
2
|
+
# The document store helps resolve URI-based JSON pointers by storing IDs
|
3
|
+
# that we've seen in the schema.
|
4
|
+
#
|
5
|
+
# Each URI tuple also contains a pointer map that helps speed up expansions
|
6
|
+
# that have already happened and handles cyclic dependencies. Store a
|
7
|
+
# reference to the top-level schema before doing anything else.
|
8
|
+
class DocumentStore
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@schema_map = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_schema(schema)
|
16
|
+
raise ArgumentError, "can't add nil URI" if schema.uri.nil?
|
17
|
+
uri = schema.uri.chomp('#')
|
18
|
+
@schema_map[uri] = schema
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
@schema_map.each { |k, v| yield(k, v) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def lookup_schema(uri)
|
26
|
+
uri = uri.chomp('#')
|
27
|
+
@schema_map[uri]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module JsonSchema
|
2
|
+
class Error < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class AggregateError < Error
|
6
|
+
attr_accessor :errors
|
7
|
+
|
8
|
+
def initialize(errors)
|
9
|
+
@errors = errors
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
@errors.join(" ")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class SchemaError < Error
|
18
|
+
attr_accessor :message, :schema, :type
|
19
|
+
|
20
|
+
def self.aggregate(errors)
|
21
|
+
errors.map(&:to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(schema, message, type)
|
25
|
+
@schema = schema
|
26
|
+
@message = message
|
27
|
+
@type = type
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
if schema && schema.pointer
|
32
|
+
"#{schema.pointer}: #{message}"
|
33
|
+
else
|
34
|
+
message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ValidationError < SchemaError
|
40
|
+
attr_accessor :data, :path, :sub_errors
|
41
|
+
|
42
|
+
def initialize(schema, path, message, type, options = {})
|
43
|
+
super(schema, message, type)
|
44
|
+
@path = path
|
45
|
+
|
46
|
+
# TODO: change to named optional arguments when Ruby 1.9 support is
|
47
|
+
# removed
|
48
|
+
@data = options[:data]
|
49
|
+
@sub_errors = options[:sub_errors]
|
50
|
+
end
|
51
|
+
|
52
|
+
def pointer
|
53
|
+
path.join("/")
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
"#{pointer}: failed schema #{schema.pointer}: #{message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ErrorFormatter
|
62
|
+
def to_list(list)
|
63
|
+
words_connector = ', '
|
64
|
+
two_words_connector = ' or '
|
65
|
+
last_word_connector = ', or '
|
66
|
+
|
67
|
+
length = list.length
|
68
|
+
joined_list = case length
|
69
|
+
when 1
|
70
|
+
list[0]
|
71
|
+
when 2
|
72
|
+
"#{list[0]}#{two_words_connector}#{list[1]}"
|
73
|
+
else
|
74
|
+
"#{list[0...-1].join(words_connector)}#{last_word_connector}#{list[-1]}"
|
75
|
+
end
|
76
|
+
|
77
|
+
if joined_list[0] =~ /^[aeiou]/
|
78
|
+
"an #{joined_list}"
|
79
|
+
else
|
80
|
+
"a #{joined_list}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
module_function :to_list
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,390 @@
|
|
1
|
+
require_relative "../json_reference"
|
2
|
+
require_relative "validator"
|
3
|
+
|
4
|
+
module JsonSchema
|
5
|
+
class Parser
|
6
|
+
ALLOWED_TYPES = %w{any array boolean integer number null object string}
|
7
|
+
BOOLEAN = [FalseClass, TrueClass]
|
8
|
+
FORMATS = JsonSchema::Validator::DEFAULT_FORMAT_VALIDATORS.keys
|
9
|
+
FRIENDLY_TYPES = {
|
10
|
+
Array => "array",
|
11
|
+
FalseClass => "boolean",
|
12
|
+
Float => "number",
|
13
|
+
Hash => "object",
|
14
|
+
Integer => "integer",
|
15
|
+
NilClass => "null",
|
16
|
+
String => "string",
|
17
|
+
TrueClass => "boolean",
|
18
|
+
}
|
19
|
+
|
20
|
+
# Reuse these frozen objects to avoid allocations
|
21
|
+
EMPTY_ARRAY = [].freeze
|
22
|
+
EMPTY_HASH = {}.freeze
|
23
|
+
|
24
|
+
attr_accessor :errors
|
25
|
+
|
26
|
+
# Basic parsing of a schema. May return a malformed schema! (Use `#parse!`
|
27
|
+
# to raise errors instead).
|
28
|
+
def parse(data, parent = nil)
|
29
|
+
# while #parse_data is recursed into for many schemas over the same
|
30
|
+
# object, the @errors array is an instance-wide accumulator
|
31
|
+
@errors = []
|
32
|
+
|
33
|
+
schema = parse_data(data, parent, "#")
|
34
|
+
if @errors.count == 0
|
35
|
+
schema
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse!(data, parent = nil)
|
42
|
+
schema = parse(data, parent)
|
43
|
+
if !schema
|
44
|
+
raise AggregateError.new(@errors)
|
45
|
+
end
|
46
|
+
schema
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_uri(id, parent_uri)
|
52
|
+
# kill any trailing slashes
|
53
|
+
if id
|
54
|
+
# may look like: http://json-schema.org/draft-04/hyper-schema#
|
55
|
+
uri = URI.parse(id)
|
56
|
+
# make sure there is no `#` suffix
|
57
|
+
uri.fragment = nil
|
58
|
+
# if id is defined as absolute, the schema's URI stays absolute
|
59
|
+
if uri.absolute? || uri.path[0] == "/"
|
60
|
+
uri.to_s.chomp("/")
|
61
|
+
# otherwise build it according to the parent's URI
|
62
|
+
elsif parent_uri
|
63
|
+
# make sure we don't end up with duplicate slashes
|
64
|
+
parent_uri = parent_uri.chomp("/")
|
65
|
+
parent_uri + "/" + id
|
66
|
+
else
|
67
|
+
"/"
|
68
|
+
end
|
69
|
+
# if id is missing, it's defined as its parent schema's URI
|
70
|
+
elsif parent_uri
|
71
|
+
parent_uri
|
72
|
+
else
|
73
|
+
"/"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_additional_items(schema)
|
78
|
+
if schema.additional_items
|
79
|
+
# an object indicates a schema that will be used to parse any
|
80
|
+
# items not listed in `items`
|
81
|
+
if schema.additional_items.is_a?(Hash)
|
82
|
+
schema.additional_items = parse_data(
|
83
|
+
schema.additional_items,
|
84
|
+
schema,
|
85
|
+
"additionalItems"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
# otherwise, leave as boolean
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_additional_properties(schema)
|
93
|
+
if schema.additional_properties
|
94
|
+
# an object indicates a schema that will be used to parse any
|
95
|
+
# properties not listed in `properties`
|
96
|
+
if schema.additional_properties.is_a?(Hash)
|
97
|
+
schema.additional_properties = parse_data(
|
98
|
+
schema.additional_properties,
|
99
|
+
schema,
|
100
|
+
"additionalProperties"
|
101
|
+
)
|
102
|
+
end
|
103
|
+
# otherwise, leave as boolean
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_all_of(schema)
|
108
|
+
if schema.all_of && !schema.all_of.empty?
|
109
|
+
schema.all_of = schema.all_of.each_with_index.
|
110
|
+
map { |s, i| parse_data(s, schema, "allOf/#{i}") }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_any_of(schema)
|
115
|
+
if schema.any_of && !schema.any_of.empty?
|
116
|
+
schema.any_of = schema.any_of.each_with_index.
|
117
|
+
map { |s, i| parse_data(s, schema, "anyOf/#{i}") }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_one_of(schema)
|
122
|
+
if schema.one_of && !schema.one_of.empty?
|
123
|
+
schema.one_of = schema.one_of.each_with_index.
|
124
|
+
map { |s, i| parse_data(s, schema, "oneOf/#{i}") }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_data(data, parent, fragment)
|
129
|
+
if !data.is_a?(Hash)
|
130
|
+
# it would be nice to make this message more specific/nicer (at best it
|
131
|
+
# points to the wrong schema)
|
132
|
+
message = %{#{data.inspect} is not a valid schema.}
|
133
|
+
@errors << SchemaError.new(parent, message, :schema_not_found)
|
134
|
+
elsif ref = data["$ref"]
|
135
|
+
schema = Schema.new
|
136
|
+
schema.fragment = fragment
|
137
|
+
schema.parent = parent
|
138
|
+
schema.reference = JsonReference::Reference.new(ref)
|
139
|
+
else
|
140
|
+
schema = parse_schema(data, parent, fragment)
|
141
|
+
end
|
142
|
+
|
143
|
+
schema
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_definitions(schema)
|
147
|
+
if schema.definitions && !schema.definitions.empty?
|
148
|
+
# leave the original data reference intact
|
149
|
+
schema.definitions = schema.definitions.dup
|
150
|
+
schema.definitions.each do |key, definition|
|
151
|
+
subschema = parse_data(definition, schema, "definitions/#{key}")
|
152
|
+
schema.definitions[key] = subschema
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_dependencies(schema)
|
158
|
+
if schema.dependencies && !schema.dependencies.empty?
|
159
|
+
# leave the original data reference intact
|
160
|
+
schema.dependencies = schema.dependencies.dup
|
161
|
+
schema.dependencies.each do |k, s|
|
162
|
+
# may be Array, String (simple dependencies), or Hash (schema
|
163
|
+
# dependency)
|
164
|
+
if s.is_a?(Hash)
|
165
|
+
schema.dependencies[k] = parse_data(s, schema, "dependencies")
|
166
|
+
elsif s.is_a?(String)
|
167
|
+
# just normalize all simple dependencies to arrays
|
168
|
+
schema.dependencies[k] = [s]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def parse_items(schema)
|
175
|
+
if schema.items
|
176
|
+
# tuple validation: an array of schemas
|
177
|
+
if schema.items.is_a?(Array)
|
178
|
+
schema.items = schema.items.each_with_index.
|
179
|
+
map { |s, i| parse_data(s, schema, "items/#{i}") }
|
180
|
+
# list validation: a single schema
|
181
|
+
else
|
182
|
+
schema.items = parse_data(schema.items, schema, "items")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_links(schema)
|
188
|
+
if schema.links && !schema.links.empty?
|
189
|
+
schema.links = schema.links.each_with_index.map { |l, i|
|
190
|
+
link = Schema::Link.new
|
191
|
+
link.parent = schema
|
192
|
+
link.fragment = "links/#{i}"
|
193
|
+
|
194
|
+
link.data = l
|
195
|
+
|
196
|
+
# any parsed schema is automatically expanded
|
197
|
+
link.expanded = true
|
198
|
+
|
199
|
+
link.uri = nil
|
200
|
+
|
201
|
+
link.description = l["description"]
|
202
|
+
link.enc_type = l["encType"]
|
203
|
+
link.href = l["href"]
|
204
|
+
link.method = l["method"] ? l["method"].downcase.to_sym : nil
|
205
|
+
link.rel = l["rel"]
|
206
|
+
link.title = l["title"]
|
207
|
+
link.media_type = l["mediaType"]
|
208
|
+
|
209
|
+
if l["schema"]
|
210
|
+
link.schema = parse_data(l["schema"], schema, "links/#{i}/schema")
|
211
|
+
end
|
212
|
+
|
213
|
+
if l["targetSchema"]
|
214
|
+
link.target_schema =
|
215
|
+
parse_data(l["targetSchema"], schema, "links/#{i}/targetSchema")
|
216
|
+
end
|
217
|
+
|
218
|
+
if l["jobSchema"]
|
219
|
+
link.job_schema =
|
220
|
+
parse_data(l["jobSchema"], schema, "links/#{i}/jobSchema")
|
221
|
+
end
|
222
|
+
|
223
|
+
link
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def parse_media(schema)
|
229
|
+
if data = schema.media
|
230
|
+
schema.media = Schema::Media.new
|
231
|
+
schema.media.binary_encoding = data["binaryEncoding"]
|
232
|
+
schema.media.type = data["type"]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def parse_not(schema)
|
237
|
+
if schema.not
|
238
|
+
schema.not = parse_data(schema.not, schema, "not")
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def parse_pattern_properties(schema)
|
243
|
+
if schema.pattern_properties && !schema.pattern_properties.empty?
|
244
|
+
# leave the original data reference intact
|
245
|
+
properties = schema.pattern_properties.dup
|
246
|
+
properties = properties.map do |k, s|
|
247
|
+
[parse_regex(schema, k), parse_data(s, schema, "patternProperties/#{k}")]
|
248
|
+
end
|
249
|
+
schema.pattern_properties = Hash[*properties.flatten]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def parse_regex(schema, regex)
|
254
|
+
case JsonSchema.configuration.validate_regex_with
|
255
|
+
when :'ecma-re-validator'
|
256
|
+
unless EcmaReValidator.valid?(regex)
|
257
|
+
message = %{#{regex.inspect} is not an ECMA-262 regular expression.}
|
258
|
+
@errors << SchemaError.new(schema, message, :regex_failed)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
Regexp.new(regex)
|
262
|
+
end
|
263
|
+
|
264
|
+
def parse_properties(schema)
|
265
|
+
# leave the original data reference intact
|
266
|
+
if schema.properties && schema.properties.is_a?(Hash) && !schema.properties.empty?
|
267
|
+
schema.properties = schema.properties.dup
|
268
|
+
schema.properties.each do |key, definition|
|
269
|
+
subschema = parse_data(definition, schema, "properties/#{key}")
|
270
|
+
schema.properties[key] = subschema
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def parse_schema(data, parent, fragment)
|
276
|
+
schema = Schema.new
|
277
|
+
schema.fragment = fragment
|
278
|
+
schema.parent = parent
|
279
|
+
|
280
|
+
schema.data = data
|
281
|
+
schema.id = validate_type(schema, [String], "id")
|
282
|
+
|
283
|
+
# any parsed schema is automatically expanded
|
284
|
+
schema.expanded = true
|
285
|
+
|
286
|
+
# build URI early so we can reference it in errors
|
287
|
+
schema.uri = build_uri(schema.id, parent ? parent.uri : nil)
|
288
|
+
|
289
|
+
schema.title = validate_type(schema, [String], "title")
|
290
|
+
schema.description = validate_type(schema, [String], "description")
|
291
|
+
schema.default = schema.data["default"]
|
292
|
+
|
293
|
+
# validation: any
|
294
|
+
schema.all_of = validate_type(schema, [Array], "allOf") || EMPTY_ARRAY
|
295
|
+
schema.any_of = validate_type(schema, [Array], "anyOf") || EMPTY_ARRAY
|
296
|
+
schema.definitions = validate_type(schema, [Hash], "definitions") || EMPTY_HASH
|
297
|
+
schema.enum = validate_type(schema, [Array], "enum")
|
298
|
+
schema.one_of = validate_type(schema, [Array], "oneOf") || EMPTY_ARRAY
|
299
|
+
schema.not = validate_type(schema, [Hash], "not")
|
300
|
+
schema.type = validate_type(schema, [Array, String], "type")
|
301
|
+
schema.type = [schema.type] if schema.type.is_a?(String)
|
302
|
+
validate_known_type!(schema)
|
303
|
+
|
304
|
+
# validation: array
|
305
|
+
schema.additional_items = validate_type(schema, BOOLEAN + [Hash], "additionalItems")
|
306
|
+
schema.items = validate_type(schema, [Array, Hash], "items")
|
307
|
+
schema.max_items = validate_type(schema, [Integer], "maxItems")
|
308
|
+
schema.min_items = validate_type(schema, [Integer], "minItems")
|
309
|
+
schema.unique_items = validate_type(schema, BOOLEAN, "uniqueItems")
|
310
|
+
|
311
|
+
# validation: number/integer
|
312
|
+
schema.max = validate_type(schema, [Float, Integer], "maximum")
|
313
|
+
schema.max_exclusive = validate_type(schema, BOOLEAN, "exclusiveMaximum")
|
314
|
+
schema.min = validate_type(schema, [Float, Integer], "minimum")
|
315
|
+
schema.min_exclusive = validate_type(schema, BOOLEAN, "exclusiveMinimum")
|
316
|
+
schema.multiple_of = validate_type(schema, [Float, Integer], "multipleOf")
|
317
|
+
|
318
|
+
# validation: object
|
319
|
+
schema.additional_properties =
|
320
|
+
validate_type(schema, BOOLEAN + [Hash], "additionalProperties")
|
321
|
+
schema.dependencies = validate_type(schema, [Hash], "dependencies") || EMPTY_HASH
|
322
|
+
schema.max_properties = validate_type(schema, [Integer], "maxProperties")
|
323
|
+
schema.min_properties = validate_type(schema, [Integer], "minProperties")
|
324
|
+
schema.pattern_properties = validate_type(schema, [Hash], "patternProperties") || EMPTY_HASH
|
325
|
+
schema.properties = validate_type(schema, [Hash], "properties") || EMPTY_HASH
|
326
|
+
schema.required = validate_type(schema, [Array], "required")
|
327
|
+
schema.strict_properties = validate_type(schema, BOOLEAN, "strictProperties")
|
328
|
+
|
329
|
+
# validation: string
|
330
|
+
schema.format = validate_type(schema, [String], "format")
|
331
|
+
schema.max_length = validate_type(schema, [Integer], "maxLength")
|
332
|
+
schema.min_length = validate_type(schema, [Integer], "minLength")
|
333
|
+
schema.pattern = validate_type(schema, [String], "pattern")
|
334
|
+
schema.pattern = parse_regex(schema, schema.pattern) if schema.pattern
|
335
|
+
validate_format(schema, schema.format) if schema.format
|
336
|
+
|
337
|
+
# hyperschema
|
338
|
+
schema.links = validate_type(schema, [Array], "links")
|
339
|
+
schema.media = validate_type(schema, [Hash], "media")
|
340
|
+
schema.path_start = validate_type(schema, [String], "pathStart")
|
341
|
+
schema.read_only = validate_type(schema, BOOLEAN, "readOnly")
|
342
|
+
|
343
|
+
parse_additional_items(schema)
|
344
|
+
parse_additional_properties(schema)
|
345
|
+
parse_all_of(schema)
|
346
|
+
parse_any_of(schema)
|
347
|
+
parse_one_of(schema)
|
348
|
+
parse_definitions(schema)
|
349
|
+
parse_dependencies(schema)
|
350
|
+
parse_items(schema)
|
351
|
+
parse_links(schema)
|
352
|
+
parse_media(schema)
|
353
|
+
parse_not(schema)
|
354
|
+
parse_pattern_properties(schema)
|
355
|
+
parse_properties(schema)
|
356
|
+
|
357
|
+
schema
|
358
|
+
end
|
359
|
+
|
360
|
+
def validate_known_type!(schema)
|
361
|
+
if schema.type
|
362
|
+
if !(bad_types = schema.type - ALLOWED_TYPES).empty?
|
363
|
+
message = %{Unknown types: #{bad_types.sort.join(", ")}.}
|
364
|
+
@errors << SchemaError.new(schema, message, :unknown_type)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def validate_type(schema, types, field)
|
370
|
+
friendly_types =
|
371
|
+
types.map { |t| FRIENDLY_TYPES[t] || t }.sort.uniq.join("/")
|
372
|
+
value = schema.data[field]
|
373
|
+
if !value.nil? && !types.any? { |t| value.is_a?(t) }
|
374
|
+
message = %{#{value.inspect} is not a valid "#{field}", must be a #{friendly_types}.}
|
375
|
+
@errors << SchemaError.new(schema, message, :invalid_type)
|
376
|
+
nil
|
377
|
+
else
|
378
|
+
value
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def validate_format(schema, format)
|
383
|
+
valid_formats = FORMATS + JsonSchema.configuration.custom_formats.keys
|
384
|
+
return if valid_formats.include?(format)
|
385
|
+
|
386
|
+
message = %{#{format.inspect} is not a valid format, must be one of #{valid_formats.join(', ')}.}
|
387
|
+
@errors << SchemaError.new(schema, message, :unknown_format)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|