dato_json_schema 0.20.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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