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.
- 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,606 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module JsonSchema
|
4
|
+
class Validator
|
5
|
+
attr_accessor :errors
|
6
|
+
|
7
|
+
def initialize(schema)
|
8
|
+
@schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(data, fail_fast: false)
|
12
|
+
@errors = []
|
13
|
+
@visits = {}
|
14
|
+
@fail_fast = fail_fast
|
15
|
+
|
16
|
+
# This dynamically creates the "strict_or_fast_and" method which is used
|
17
|
+
# throughout the validator to combine the previous validation result with
|
18
|
+
# another validation check.
|
19
|
+
# Logic wise, we could simply define this method without meta programming
|
20
|
+
# and decide every time to either call fast_and or strict_end.
|
21
|
+
# Unfortunately this has a small overhead, that adds up over the runtime
|
22
|
+
# of the validator – about 5% if we check @fail_fast everytime.
|
23
|
+
# For more details, please see https://github.com/brandur/json_schema/pull/96
|
24
|
+
and_operation = method(@fail_fast ? :fast_and : :strict_and)
|
25
|
+
define_singleton_method(:strict_or_fast_and, and_operation)
|
26
|
+
|
27
|
+
catch(:fail_fast) do
|
28
|
+
validate_data(@schema, data, @errors, ['#'])
|
29
|
+
end
|
30
|
+
@errors.size == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate!(data, fail_fast: false)
|
34
|
+
if !validate(data, fail_fast: fail_fast)
|
35
|
+
raise AggregateError.new(@errors)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def first_visit(schema, errors, path)
|
42
|
+
true
|
43
|
+
# removed until more comprehensive testing can be performed .. this is
|
44
|
+
# currently causing validation loop detections to go off on all non-trivial
|
45
|
+
# schemas
|
46
|
+
=begin
|
47
|
+
key = "#{schema.object_id}-#{schema.pointer}-#{path.join("/")}"
|
48
|
+
if !@visits.key?(key)
|
49
|
+
@visits[key] = true
|
50
|
+
true
|
51
|
+
else
|
52
|
+
message = %{Validation loop detected.}
|
53
|
+
errors << ValidationError.new(schema, path, message, :loop_detected)
|
54
|
+
false
|
55
|
+
end
|
56
|
+
=end
|
57
|
+
end
|
58
|
+
|
59
|
+
# for use with additionalProperties and strictProperties
|
60
|
+
def get_extra_keys(schema, data)
|
61
|
+
extra = data.keys - schema.properties.keys
|
62
|
+
|
63
|
+
if schema.pattern_properties
|
64
|
+
schema.pattern_properties.keys.each do |pattern|
|
65
|
+
extra -= extra.select { |k| k =~ pattern }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
extra
|
70
|
+
end
|
71
|
+
|
72
|
+
# works around &&'s "lazy" behavior
|
73
|
+
def strict_and(valid_old, valid_new)
|
74
|
+
valid_old && valid_new
|
75
|
+
end
|
76
|
+
|
77
|
+
def fast_and(valid_old, valid_new)
|
78
|
+
throw :fail_fast, false if !valid_new
|
79
|
+
valid_old && valid_new
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_data(schema, data, errors, path)
|
83
|
+
valid = true
|
84
|
+
# detect a validation loop
|
85
|
+
if !first_visit(schema, errors, path)
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
|
89
|
+
# validation: any
|
90
|
+
valid = strict_or_fast_and valid, validate_all_of(schema, data, errors, path)
|
91
|
+
valid = strict_or_fast_and valid, validate_any_of(schema, data, errors, path)
|
92
|
+
valid = strict_or_fast_and valid, validate_enum(schema, data, errors, path)
|
93
|
+
valid = strict_or_fast_and valid, validate_one_of(schema, data, errors, path)
|
94
|
+
valid = strict_or_fast_and valid, validate_not(schema, data, errors, path)
|
95
|
+
valid = strict_or_fast_and valid, validate_type(schema, data, errors, path)
|
96
|
+
|
97
|
+
# validation: array
|
98
|
+
if data.is_a?(Array)
|
99
|
+
valid = strict_or_fast_and valid, validate_items(schema, data, errors, path)
|
100
|
+
valid = strict_or_fast_and valid, validate_max_items(schema, data, errors, path)
|
101
|
+
valid = strict_or_fast_and valid, validate_min_items(schema, data, errors, path)
|
102
|
+
valid = strict_or_fast_and valid, validate_unique_items(schema, data, errors, path)
|
103
|
+
end
|
104
|
+
|
105
|
+
# validation: integer/number
|
106
|
+
if data.is_a?(Float) || data.is_a?(Integer)
|
107
|
+
valid = strict_or_fast_and valid, validate_max(schema, data, errors, path)
|
108
|
+
valid = strict_or_fast_and valid, validate_min(schema, data, errors, path)
|
109
|
+
valid = strict_or_fast_and valid, validate_multiple_of(schema, data, errors, path)
|
110
|
+
end
|
111
|
+
|
112
|
+
# validation: object
|
113
|
+
if data.is_a?(Hash)
|
114
|
+
valid = strict_or_fast_and valid, validate_additional_properties(schema, data, errors, path)
|
115
|
+
valid = strict_or_fast_and valid, validate_dependencies(schema, data, errors, path)
|
116
|
+
valid = strict_or_fast_and valid, validate_max_properties(schema, data, errors, path)
|
117
|
+
valid = strict_or_fast_and valid, validate_min_properties(schema, data, errors, path)
|
118
|
+
valid = strict_or_fast_and valid, validate_pattern_properties(schema, data, errors, path)
|
119
|
+
valid = strict_or_fast_and valid, validate_properties(schema, data, errors, path)
|
120
|
+
valid = strict_or_fast_and valid, validate_required(schema, data, errors, path, schema.required)
|
121
|
+
valid = strict_or_fast_and valid, validate_strict_properties(schema, data, errors, path)
|
122
|
+
end
|
123
|
+
|
124
|
+
# validation: string
|
125
|
+
if data.is_a?(String)
|
126
|
+
valid = strict_or_fast_and valid, validate_format(schema, data, errors, path)
|
127
|
+
valid = strict_or_fast_and valid, validate_max_length(schema, data, errors, path)
|
128
|
+
valid = strict_or_fast_and valid, validate_min_length(schema, data, errors, path)
|
129
|
+
valid = strict_or_fast_and valid, validate_pattern(schema, data, errors, path)
|
130
|
+
end
|
131
|
+
|
132
|
+
valid
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_additional_properties(schema, data, errors, path)
|
136
|
+
return true if schema.additional_properties == true
|
137
|
+
|
138
|
+
# schema indicates that all properties not in `properties` should be
|
139
|
+
# validated according to subschema
|
140
|
+
if schema.additional_properties.is_a?(Schema)
|
141
|
+
extra = get_extra_keys(schema, data)
|
142
|
+
validations = extra.map do |key|
|
143
|
+
validate_data(schema.additional_properties, data[key], errors, path + [key])
|
144
|
+
end
|
145
|
+
|
146
|
+
# true only if all keys validate
|
147
|
+
validations.all?
|
148
|
+
|
149
|
+
# boolean indicates whether additional properties are allowed
|
150
|
+
else
|
151
|
+
validate_extra(schema, data, errors, path)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def validate_all_of(schema, data, errors, path)
|
156
|
+
return true if schema.all_of.empty?
|
157
|
+
|
158
|
+
# We've kept this feature behind a configuration flag for now because
|
159
|
+
# there is some performance implication to producing each sub error.
|
160
|
+
# Normally we can short circuit the validation after encountering only
|
161
|
+
# one problem, but here we have to evaluate all subschemas every time.
|
162
|
+
if JsonSchema.configuration.all_of_sub_errors && !@fail_fast
|
163
|
+
sub_errors = []
|
164
|
+
valid = schema.all_of.map do |subschema|
|
165
|
+
current_sub_errors = []
|
166
|
+
sub_errors << current_sub_errors
|
167
|
+
validate_data(subschema, data, current_sub_errors, path)
|
168
|
+
end.all?
|
169
|
+
else
|
170
|
+
sub_errors = nil
|
171
|
+
valid = schema.all_of.all? do |subschema|
|
172
|
+
validate_data(subschema, data, errors, path)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
message = %{Not all subschemas of "allOf" matched.}
|
177
|
+
errors << ValidationError.new(schema, path, message, :all_of_failed,
|
178
|
+
sub_errors: sub_errors, data: data) if !valid
|
179
|
+
valid
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate_any_of(schema, data, errors, path)
|
183
|
+
return true if schema.any_of.empty?
|
184
|
+
|
185
|
+
sub_errors = schema.any_of.map do |subschema|
|
186
|
+
current_sub_errors = []
|
187
|
+
valid = catch(:fail_fast) do
|
188
|
+
validate_data(subschema, data, current_sub_errors, path)
|
189
|
+
end
|
190
|
+
return true if valid
|
191
|
+
current_sub_errors
|
192
|
+
end
|
193
|
+
|
194
|
+
message = %{No subschema in "anyOf" matched.}
|
195
|
+
errors << ValidationError.new(schema, path, message, :any_of_failed,
|
196
|
+
sub_errors: sub_errors, data: data)
|
197
|
+
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
def validate_dependencies(schema, data, errors, path)
|
202
|
+
return true if schema.dependencies.empty?
|
203
|
+
result = schema.dependencies.map do |key, obj|
|
204
|
+
# if the key is not present, the dependency is fulfilled by definition
|
205
|
+
next true unless data[key]
|
206
|
+
if obj.is_a?(Schema)
|
207
|
+
validate_data(obj, data, errors, path)
|
208
|
+
else
|
209
|
+
# if not a schema, value is an array of required fields
|
210
|
+
validate_required(schema, data, errors, path, obj)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
result.all?
|
214
|
+
end
|
215
|
+
|
216
|
+
def validate_format(schema, data, errors, path)
|
217
|
+
return true unless schema.format
|
218
|
+
validator = (
|
219
|
+
JsonSchema.configuration.custom_formats[schema.format] ||
|
220
|
+
DEFAULT_FORMAT_VALIDATORS[schema.format]
|
221
|
+
)
|
222
|
+
if validator[data]
|
223
|
+
true
|
224
|
+
else
|
225
|
+
message = %{#{data} is not a valid #{schema.format}.}
|
226
|
+
errors << ValidationError.new(schema, path, message, :invalid_format, data: data)
|
227
|
+
false
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def validate_enum(schema, data, errors, path)
|
232
|
+
return true unless schema.enum
|
233
|
+
if schema.enum.include?(data)
|
234
|
+
true
|
235
|
+
else
|
236
|
+
message = %{#{data} is not a member of #{schema.enum}.}
|
237
|
+
errors << ValidationError.new(schema, path, message, :invalid_type, data: data)
|
238
|
+
false
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def validate_extra(schema, data, errors, path)
|
243
|
+
extra = get_extra_keys(schema, data)
|
244
|
+
if extra.empty?
|
245
|
+
true
|
246
|
+
else
|
247
|
+
|
248
|
+
message = %{"#{extra.sort.join('", "')}" } +
|
249
|
+
(extra.length == 1 ? "is not a" : "are not") +
|
250
|
+
%{ permitted key} +
|
251
|
+
(extra.length == 1 ? "." : "s.")
|
252
|
+
errors << ValidationError.new(schema, path, message, :invalid_keys)
|
253
|
+
false
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def validate_items(schema, data, errors, path)
|
258
|
+
return true unless schema.items
|
259
|
+
if schema.items.is_a?(Array)
|
260
|
+
if data.size < schema.items.count
|
261
|
+
message = %{#{schema.items.count} item} +
|
262
|
+
(schema.items.count == 1 ? "" : "s") +
|
263
|
+
%{ required; only #{data.size} } +
|
264
|
+
(data.size == 1 ? "was" : "were") +
|
265
|
+
%{ supplied.}
|
266
|
+
errors << ValidationError.new(schema, path, message, :min_items_failed, data: data)
|
267
|
+
false
|
268
|
+
elsif data.size > schema.items.count && !schema.additional_items?
|
269
|
+
message = %{No more than #{schema.items.count} item} +
|
270
|
+
(schema.items.count == 1 ? " is" : "s are") +
|
271
|
+
%{ allowed; #{data.size} } +
|
272
|
+
(data.size > 1 ? "were" : "was") +
|
273
|
+
%{ supplied.}
|
274
|
+
errors << ValidationError.new(schema, path, message, :max_items_failed, data: data)
|
275
|
+
false
|
276
|
+
else
|
277
|
+
valid = true
|
278
|
+
if data.size > schema.items.count && schema.additional_items.is_a?(Schema)
|
279
|
+
(schema.items.count..data.count - 1).each do |i|
|
280
|
+
valid = strict_or_fast_and valid,
|
281
|
+
validate_data(schema.additional_items, data[i], errors, path + [i])
|
282
|
+
end
|
283
|
+
end
|
284
|
+
schema.items.each_with_index do |subschema, i|
|
285
|
+
valid = strict_or_fast_and valid,
|
286
|
+
validate_data(subschema, data[i], errors, path + [i])
|
287
|
+
end
|
288
|
+
valid
|
289
|
+
end
|
290
|
+
else
|
291
|
+
valid = true
|
292
|
+
data.each_with_index do |value, i|
|
293
|
+
valid = strict_or_fast_and valid,
|
294
|
+
validate_data(schema.items, value, errors, path + [i])
|
295
|
+
end
|
296
|
+
valid
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def validate_max(schema, data, errors, path)
|
301
|
+
return true unless schema.max
|
302
|
+
if schema.max_exclusive? && data < schema.max
|
303
|
+
true
|
304
|
+
elsif !schema.max_exclusive? && data <= schema.max
|
305
|
+
true
|
306
|
+
else
|
307
|
+
message = %{#{data} must be less than} +
|
308
|
+
(schema.max_exclusive? ? "" : " or equal to") +
|
309
|
+
%{ #{schema.max}.}
|
310
|
+
errors << ValidationError.new(schema, path, message, :max_failed, data: data)
|
311
|
+
false
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def validate_max_items(schema, data, errors, path)
|
316
|
+
return true unless schema.max_items
|
317
|
+
if data.size <= schema.max_items
|
318
|
+
true
|
319
|
+
else
|
320
|
+
message = %{No more than #{schema.max_items} item} +
|
321
|
+
(schema.max_items == 1 ? " is" : "s are") +
|
322
|
+
%{ allowed; #{data.size} } +
|
323
|
+
(data.size == 1 ? "was" : "were")+
|
324
|
+
%{ supplied.}
|
325
|
+
errors << ValidationError.new(schema, path, message, :max_items_failed, data: data)
|
326
|
+
false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def validate_max_length(schema, data, errors, path)
|
331
|
+
return true unless schema.max_length
|
332
|
+
if data.length <= schema.max_length
|
333
|
+
true
|
334
|
+
else
|
335
|
+
message = %{Only #{schema.max_length} character} +
|
336
|
+
(schema.max_length == 1 ? " is" : "s are") +
|
337
|
+
%{ allowed; #{data.length} } +
|
338
|
+
(data.length == 1 ? "was" : "were") +
|
339
|
+
%{ supplied.}
|
340
|
+
errors << ValidationError.new(schema, path, message, :max_length_failed, data: data)
|
341
|
+
false
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def validate_max_properties(schema, data, errors, path)
|
346
|
+
return true unless schema.max_properties
|
347
|
+
if data.keys.size <= schema.max_properties
|
348
|
+
true
|
349
|
+
else
|
350
|
+
message = %{No more than #{schema.max_properties} propert} +
|
351
|
+
(schema.max_properties == 1 ? "y is" : "ies are") +
|
352
|
+
%{ allowed; #{data.keys.size} } +
|
353
|
+
(data.keys.size == 1 ? "was" : "were") +
|
354
|
+
%{ supplied.}
|
355
|
+
errors << ValidationError.new(schema, path, message, :max_properties_failed, data: data)
|
356
|
+
false
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def validate_min(schema, data, errors, path)
|
361
|
+
return true unless schema.min
|
362
|
+
if schema.min_exclusive? && data > schema.min
|
363
|
+
true
|
364
|
+
elsif !schema.min_exclusive? && data >= schema.min
|
365
|
+
true
|
366
|
+
else
|
367
|
+
message = %{#{data} must be greater than} +
|
368
|
+
(schema.min_exclusive? ? "" : " or equal to") +
|
369
|
+
%{ #{schema.min}.}
|
370
|
+
errors << ValidationError.new(schema, path, message, :min_failed, data: data)
|
371
|
+
false
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def validate_min_items(schema, data, errors, path)
|
376
|
+
return true unless schema.min_items
|
377
|
+
if data.size >= schema.min_items
|
378
|
+
true
|
379
|
+
else
|
380
|
+
message = %{#{schema.min_items} item} +
|
381
|
+
(schema.min_items == 1 ? "" : "s") +
|
382
|
+
%{ required; only #{data.size} } +
|
383
|
+
(data.size == 1 ? "was" : "were") +
|
384
|
+
%{ supplied.}
|
385
|
+
errors << ValidationError.new(schema, path, message, :min_items_failed, data: data)
|
386
|
+
false
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def validate_min_length(schema, data, errors, path)
|
391
|
+
return true unless schema.min_length
|
392
|
+
if data.length >= schema.min_length
|
393
|
+
true
|
394
|
+
else
|
395
|
+
message = %{At least #{schema.min_length} character} +
|
396
|
+
(schema.min_length == 1 ? " is" : "s are") +
|
397
|
+
%{ required; only #{data.length} } +
|
398
|
+
(data.length == 1 ? "was" : "were") +
|
399
|
+
%{ supplied.}
|
400
|
+
errors << ValidationError.new(schema, path, message, :min_length_failed, data: data)
|
401
|
+
false
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def validate_min_properties(schema, data, errors, path)
|
406
|
+
return true unless schema.min_properties
|
407
|
+
if data.keys.size >= schema.min_properties
|
408
|
+
true
|
409
|
+
else
|
410
|
+
message = %{At least #{schema.min_properties} propert}+
|
411
|
+
(schema.min_properties == 1 ? "y is" : "ies are") +
|
412
|
+
%{ required; #{data.keys.size} }+
|
413
|
+
(data.keys.size == 1 ? "was" : "were") +
|
414
|
+
%{ supplied.}
|
415
|
+
errors << ValidationError.new(schema, path, message, :min_properties_failed, data: data)
|
416
|
+
false
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def validate_multiple_of(schema, data, errors, path)
|
421
|
+
return true unless schema.multiple_of
|
422
|
+
if data % schema.multiple_of == 0
|
423
|
+
true
|
424
|
+
else
|
425
|
+
message = %{#{data} is not a multiple of #{schema.multiple_of}.}
|
426
|
+
errors << ValidationError.new(schema, path, message, :multiple_of_failed, data: data)
|
427
|
+
false
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def validate_one_of(schema, data, errors, path)
|
432
|
+
return true if schema.one_of.empty?
|
433
|
+
sub_errors = []
|
434
|
+
|
435
|
+
num_valid = schema.one_of.count do |subschema|
|
436
|
+
current_sub_errors = []
|
437
|
+
valid = catch(:fail_fast) do
|
438
|
+
validate_data(subschema, data, current_sub_errors, path)
|
439
|
+
end
|
440
|
+
sub_errors << current_sub_errors
|
441
|
+
valid
|
442
|
+
end
|
443
|
+
|
444
|
+
return true if num_valid == 1
|
445
|
+
|
446
|
+
message =
|
447
|
+
if num_valid == 0
|
448
|
+
%{No subschema in "oneOf" matched.}
|
449
|
+
else
|
450
|
+
%{More than one subschema in "oneOf" matched.}
|
451
|
+
end
|
452
|
+
errors << ValidationError.new(schema, path, message, :one_of_failed,
|
453
|
+
sub_errors: sub_errors, data: data)
|
454
|
+
|
455
|
+
false
|
456
|
+
end
|
457
|
+
|
458
|
+
def validate_not(schema, data, errors, path)
|
459
|
+
return true unless schema.not
|
460
|
+
# don't bother accumulating these errors, they'll all be worded
|
461
|
+
# incorrectly for the inverse condition
|
462
|
+
valid = !validate_data(schema.not, data, [], path)
|
463
|
+
if !valid
|
464
|
+
message = %{Matched "not" subschema.}
|
465
|
+
errors << ValidationError.new(schema, path, message, :not_failed, data: data)
|
466
|
+
end
|
467
|
+
valid
|
468
|
+
end
|
469
|
+
|
470
|
+
def validate_pattern(schema, data, errors, path)
|
471
|
+
return true unless schema.pattern
|
472
|
+
|
473
|
+
if data =~ schema.pattern
|
474
|
+
true
|
475
|
+
else
|
476
|
+
message = %{#{data} does not match #{schema.pattern.inspect}.}
|
477
|
+
errors << ValidationError.new(schema, path, message, :pattern_failed, data: data)
|
478
|
+
false
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
def validate_pattern_properties(schema, data, errors, path)
|
483
|
+
return true if schema.pattern_properties.empty?
|
484
|
+
valid = true
|
485
|
+
schema.pattern_properties.each do |pattern, subschema|
|
486
|
+
data.each do |key, value|
|
487
|
+
if key =~ pattern
|
488
|
+
valid = strict_or_fast_and valid,
|
489
|
+
validate_data(subschema, value, errors, path + [key])
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
valid
|
494
|
+
end
|
495
|
+
|
496
|
+
def validate_properties(schema, data, errors, path)
|
497
|
+
return true if schema.properties.empty?
|
498
|
+
valid = true
|
499
|
+
schema.properties.each do |key, subschema|
|
500
|
+
next unless data.key?(key)
|
501
|
+
valid = strict_or_fast_and valid,
|
502
|
+
validate_data(subschema, data[key], errors, path + [key])
|
503
|
+
end
|
504
|
+
valid
|
505
|
+
end
|
506
|
+
|
507
|
+
def validate_required(schema, data, errors, path, required)
|
508
|
+
return true if !required || required.empty?
|
509
|
+
if (missing = required - data.keys).empty?
|
510
|
+
true
|
511
|
+
else
|
512
|
+
message = %{"#{missing.sort.join('", "')}" } +
|
513
|
+
(missing.length == 1 ? "wasn't" : "weren't") +
|
514
|
+
%{ supplied.}
|
515
|
+
errors << ValidationError.new(schema, path, message, :required_failed, data: missing)
|
516
|
+
false
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def validate_strict_properties(schema, data, errors, path)
|
521
|
+
return true if !schema.strict_properties
|
522
|
+
|
523
|
+
strict_or_fast_and validate_extra(schema, data, errors, path),
|
524
|
+
validate_required(schema, data, errors, path, schema.properties.keys)
|
525
|
+
end
|
526
|
+
|
527
|
+
def validate_type(schema, data, errors, path)
|
528
|
+
return true if !schema.type || schema.type.empty?
|
529
|
+
if schema.type_parsed.any? { |t| data.is_a?(t) }
|
530
|
+
true
|
531
|
+
else
|
532
|
+
key = find_parent(schema)
|
533
|
+
message = %{For '#{key}', #{data.inspect} is not #{ErrorFormatter.to_list(schema.type)}.}
|
534
|
+
errors << ValidationError.new(schema, path, message, :invalid_type, data: data)
|
535
|
+
false
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def validate_unique_items(schema, data, errors, path)
|
540
|
+
return true unless schema.unique_items?
|
541
|
+
if data.size == data.uniq.size
|
542
|
+
true
|
543
|
+
else
|
544
|
+
message = %{Duplicate items are not allowed.}
|
545
|
+
errors << ValidationError.new(schema, path, message, :unique_items_failed, data: data)
|
546
|
+
false
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def find_parent(schema)
|
551
|
+
fragment = schema.fragment
|
552
|
+
key = if fragment =~ /patternProperties/
|
553
|
+
split_pointer = schema.pointer.split("/")
|
554
|
+
idx = split_pointer.index("patternProperties")
|
555
|
+
|
556
|
+
# this join mimics the fragment format below in that it's
|
557
|
+
# parent + key
|
558
|
+
if idx - 2 >= 0
|
559
|
+
parts = split_pointer[(idx - 2)..(idx - 1)]
|
560
|
+
end
|
561
|
+
|
562
|
+
# protect against a `nil` that could occur if
|
563
|
+
# `patternProperties` has no parent
|
564
|
+
parts ? parts.compact.join("/") : nil
|
565
|
+
end
|
566
|
+
key || fragment
|
567
|
+
end
|
568
|
+
|
569
|
+
DEFAULT_FORMAT_VALIDATORS = {
|
570
|
+
"date" => ->(data) { data =~ DATE_PATTERN },
|
571
|
+
"date-time" => ->(data) { data =~ DATE_TIME_PATTERN },
|
572
|
+
"email" => ->(data) { data =~ EMAIL_PATTERN },
|
573
|
+
"hostname" => ->(data) { data =~ HOSTNAME_PATTERN },
|
574
|
+
"ipv4" => ->(data) { data =~ IPV4_PATTERN },
|
575
|
+
"ipv6" => ->(data) { data =~ IPV6_PATTERN },
|
576
|
+
"regex" => ->(data) { Regexp.new(data) rescue false },
|
577
|
+
"uri" => ->(data) { URI.parse(data) rescue false },
|
578
|
+
|
579
|
+
# From the spec: a string instance is valid URI Reference (either a URI
|
580
|
+
# or a relative-reference), according to RFC3986.
|
581
|
+
#
|
582
|
+
# URI.parse will a handle a relative reference as well as an absolute
|
583
|
+
# one. Really though we should try to make "uri" more restrictive, and
|
584
|
+
# both of these could do to be more robust.
|
585
|
+
"uri-reference" => ->(data) { URI.parse(data) rescue false },
|
586
|
+
|
587
|
+
"uuid" => ->(data) { data =~ UUID_PATTERN },
|
588
|
+
}.freeze
|
589
|
+
|
590
|
+
EMAIL_PATTERN = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i
|
591
|
+
|
592
|
+
HOSTNAME_PATTERN = /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/
|
593
|
+
|
594
|
+
DATE_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
595
|
+
|
596
|
+
DATE_TIME_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-2][0-9]:[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|[\-+][0-9]{2}:[0-5][0-9])$/
|
597
|
+
|
598
|
+
# from: http://stackoverflow.com/a/17871737
|
599
|
+
IPV4_PATTERN = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/
|
600
|
+
|
601
|
+
# from: http://stackoverflow.com/a/17871737
|
602
|
+
IPV6_PATTERN = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:)$/
|
603
|
+
|
604
|
+
UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
|
605
|
+
end
|
606
|
+
end
|