dato_json_schema 0.20.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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