json_schemer 0.2.15 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -5
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +23 -12
- data/README.md +77 -2
- data/bin/hostname_character_classes +42 -0
- data/bin/rake +29 -0
- data/exe/json_schemer +62 -0
- data/json_schemer.gemspec +4 -12
- data/lib/json_schemer/cached_resolver.rb +16 -0
- data/lib/json_schemer/ecma_regexp.rb +51 -0
- data/lib/json_schemer/format/hostname.rb +58 -0
- data/lib/json_schemer/format/uri_template.rb +34 -0
- data/lib/json_schemer/format.rb +37 -26
- data/lib/json_schemer/schema/base.rb +152 -112
- data/lib/json_schemer/schema/draft4.json +149 -0
- data/lib/json_schemer/schema/draft6.json +155 -0
- data/lib/json_schemer/schema/draft7.json +172 -0
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +37 -18
- metadata +32 -21
- data/lib/json_schemer/cached_ref_resolver.rb +0 -14
data/lib/json_schemer/format.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module JSONSchemer
|
3
3
|
module Format
|
4
|
+
include Hostname
|
5
|
+
include URITemplate
|
6
|
+
|
4
7
|
# this is no good
|
5
8
|
EMAIL_REGEX = /\A[^@\s]+@([\p{L}\d-]+\.)+[\p{L}\d\-]{2,}\z/i.freeze
|
6
|
-
LABEL_REGEX_STRING = '[\p{L}\p{N}]([\p{L}\p{N}\-]*[\p{L}\p{N}])?'
|
7
|
-
HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze
|
8
9
|
JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
|
9
10
|
JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
|
10
11
|
RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
|
11
12
|
DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
|
12
|
-
|
13
|
+
HOUR_24_REGEX = /T24/.freeze
|
14
|
+
LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze
|
15
|
+
IP_REGEX = /\A[\h:.]+\z/.freeze
|
16
|
+
INVALID_QUERY_REGEX = /\s/.freeze
|
13
17
|
|
14
18
|
def valid_spec_format?(data, format)
|
15
19
|
case format
|
@@ -28,9 +32,9 @@ module JSONSchemer
|
|
28
32
|
when 'idn-hostname'
|
29
33
|
valid_hostname?(data)
|
30
34
|
when 'ipv4'
|
31
|
-
valid_ip?(data,
|
35
|
+
valid_ip?(data, Socket::AF_INET)
|
32
36
|
when 'ipv6'
|
33
|
-
valid_ip?(data,
|
37
|
+
valid_ip?(data, Socket::AF_INET6)
|
34
38
|
when 'uri'
|
35
39
|
valid_uri?(data)
|
36
40
|
when 'uri-reference'
|
@@ -46,7 +50,9 @@ module JSONSchemer
|
|
46
50
|
when 'relative-json-pointer'
|
47
51
|
valid_relative_json_pointer?(data)
|
48
52
|
when 'regex'
|
49
|
-
|
53
|
+
valid_regex?(data)
|
54
|
+
else
|
55
|
+
raise UnknownFormat, format
|
50
56
|
end
|
51
57
|
end
|
52
58
|
|
@@ -58,25 +64,24 @@ module JSONSchemer
|
|
58
64
|
end
|
59
65
|
|
60
66
|
def valid_date_time?(data)
|
61
|
-
|
67
|
+
return false if HOUR_24_REGEX.match?(data)
|
68
|
+
datetime = DateTime.rfc3339(data)
|
69
|
+
return false if LEAP_SECOND_REGEX.match?(data) && datetime.to_time.utc.strftime('%H:%M') != '23:59'
|
62
70
|
DATE_TIME_OFFSET_REGEX.match?(data)
|
63
|
-
rescue ArgumentError
|
64
|
-
raise e unless e.message == 'invalid date'
|
71
|
+
rescue ArgumentError
|
65
72
|
false
|
66
73
|
end
|
67
74
|
|
68
75
|
def valid_email?(data)
|
69
|
-
EMAIL_REGEX.match?(data)
|
70
|
-
|
71
|
-
|
72
|
-
def valid_hostname?(data)
|
73
|
-
HOSTNAME_REGEX.match?(data) && data.split('.').all? { |label| label.size <= 63 }
|
76
|
+
return false unless EMAIL_REGEX.match?(data)
|
77
|
+
local, _domain = data.partition('@')
|
78
|
+
!local.start_with?('.') && !local.end_with?('.') && !local.include?('..')
|
74
79
|
end
|
75
80
|
|
76
|
-
def valid_ip?(data,
|
77
|
-
|
78
|
-
|
79
|
-
rescue IPAddr::
|
81
|
+
def valid_ip?(data, family)
|
82
|
+
IPAddr.new(data, family)
|
83
|
+
IP_REGEX.match?(data)
|
84
|
+
rescue IPAddr::Error
|
80
85
|
false
|
81
86
|
end
|
82
87
|
|
@@ -101,14 +106,14 @@ module JSONSchemer
|
|
101
106
|
end
|
102
107
|
|
103
108
|
def iri_escape(data)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
data.gsub(/[^[:ascii:]]/) do |match|
|
110
|
+
us = match
|
111
|
+
tmp = +''
|
112
|
+
us.each_byte do |uc|
|
113
|
+
tmp << sprintf('%%%02X', uc)
|
114
|
+
end
|
115
|
+
tmp
|
116
|
+
end.force_encoding(Encoding::US_ASCII)
|
112
117
|
end
|
113
118
|
|
114
119
|
def valid_json_pointer?(data)
|
@@ -118,5 +123,11 @@ module JSONSchemer
|
|
118
123
|
def valid_relative_json_pointer?(data)
|
119
124
|
RELATIVE_JSON_POINTER_REGEX.match?(data)
|
120
125
|
end
|
126
|
+
|
127
|
+
def valid_regex?(data)
|
128
|
+
!!EcmaRegexp.ruby_equivalent(data)
|
129
|
+
rescue InvalidEcmaRegexp
|
130
|
+
false
|
131
|
+
end
|
121
132
|
end
|
122
133
|
end
|
@@ -4,58 +4,102 @@ module JSONSchemer
|
|
4
4
|
class Base
|
5
5
|
include Format
|
6
6
|
|
7
|
-
Instance = Struct.new(:data, :data_pointer, :schema, :schema_pointer, :
|
7
|
+
Instance = Struct.new(:data, :data_pointer, :schema, :schema_pointer, :base_uri, :before_property_validation, :after_property_validation) do
|
8
8
|
def merge(
|
9
9
|
data: self.data,
|
10
10
|
data_pointer: self.data_pointer,
|
11
11
|
schema: self.schema,
|
12
12
|
schema_pointer: self.schema_pointer,
|
13
|
-
|
14
|
-
|
13
|
+
base_uri: self.base_uri,
|
14
|
+
before_property_validation: self.before_property_validation,
|
15
|
+
after_property_validation: self.after_property_validation
|
15
16
|
)
|
16
|
-
self.class.new(data, data_pointer, schema, schema_pointer,
|
17
|
+
self.class.new(data, data_pointer, schema, schema_pointer, base_uri, before_property_validation, after_property_validation)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
21
|
ID_KEYWORD = '$id'
|
21
22
|
DEFAULT_REF_RESOLVER = proc { |uri| raise UnknownRef, uri.to_s }
|
22
23
|
NET_HTTP_REF_RESOLVER = proc { |uri| JSON.parse(Net::HTTP.get(uri)) }
|
24
|
+
RUBY_REGEXP_RESOLVER = proc { |pattern| Regexp.new(pattern) }
|
25
|
+
ECMA_REGEXP_RESOLVER = proc { |pattern| Regexp.new(EcmaRegexp.ruby_equivalent(pattern)) }
|
23
26
|
BOOLEANS = Set[true, false].freeze
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
INSERT_DEFAULT_PROPERTY = proc do |data, property, property_schema, _parent|
|
29
|
+
if !data.key?(property) && property_schema.is_a?(Hash) && property_schema.key?('default')
|
30
|
+
data[property] = property_schema.fetch('default').clone
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
JSON_POINTER_TOKEN_ESCAPE_CHARS = { '~' => '~0', '/' => '~1' }
|
35
|
+
JSON_POINTER_TOKEN_ESCAPE_REGEX = Regexp.union(JSON_POINTER_TOKEN_ESCAPE_CHARS.keys)
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def draft_name
|
39
|
+
name.split('::').last.downcase
|
40
|
+
end
|
41
|
+
|
42
|
+
def meta_schema
|
43
|
+
@meta_schema ||= JSON.parse(Pathname.new(__dir__).join("#{draft_name}.json").read).freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
def meta_schemer
|
47
|
+
@meta_schemer ||= JSONSchemer.schema(meta_schema)
|
48
|
+
end
|
49
|
+
end
|
31
50
|
|
32
51
|
def initialize(
|
33
52
|
schema,
|
53
|
+
base_uri: nil,
|
34
54
|
format: true,
|
35
55
|
insert_property_defaults: false,
|
56
|
+
before_property_validation: nil,
|
57
|
+
after_property_validation: nil,
|
36
58
|
formats: nil,
|
37
59
|
keywords: nil,
|
38
|
-
ref_resolver: DEFAULT_REF_RESOLVER
|
60
|
+
ref_resolver: DEFAULT_REF_RESOLVER,
|
61
|
+
regexp_resolver: 'ruby'
|
39
62
|
)
|
40
63
|
raise InvalidSymbolKey, 'schemas must use string keys' if schema.is_a?(Hash) && !schema.empty? && !schema.first.first.is_a?(String)
|
41
64
|
@root = schema
|
65
|
+
@base_uri = base_uri
|
42
66
|
@format = format
|
43
|
-
@
|
67
|
+
@before_property_validation = [*before_property_validation]
|
68
|
+
@before_property_validation.unshift(INSERT_DEFAULT_PROPERTY) if insert_property_defaults
|
69
|
+
@after_property_validation = [*after_property_validation]
|
44
70
|
@formats = formats
|
45
71
|
@keywords = keywords
|
46
|
-
@ref_resolver = ref_resolver == 'net/http' ?
|
72
|
+
@ref_resolver = ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
|
73
|
+
@regexp_resolver = case regexp_resolver
|
74
|
+
when 'ecma'
|
75
|
+
CachedResolver.new(&ECMA_REGEXP_RESOLVER)
|
76
|
+
when 'ruby'
|
77
|
+
CachedResolver.new(&RUBY_REGEXP_RESOLVER)
|
78
|
+
else
|
79
|
+
regexp_resolver
|
80
|
+
end
|
47
81
|
end
|
48
82
|
|
49
83
|
def valid?(data)
|
50
|
-
valid_instance?(Instance.new(data, '', root, '',
|
84
|
+
valid_instance?(Instance.new(data, '', root, '', @base_uri, @before_property_validation, @after_property_validation))
|
51
85
|
end
|
52
86
|
|
53
87
|
def validate(data)
|
54
|
-
validate_instance(Instance.new(data, '', root, '',
|
88
|
+
validate_instance(Instance.new(data, '', root, '', @base_uri, @before_property_validation, @after_property_validation))
|
89
|
+
end
|
90
|
+
|
91
|
+
def valid_schema?
|
92
|
+
self.class.meta_schemer.valid?(root)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_schema
|
96
|
+
self.class.meta_schemer.validate(root)
|
55
97
|
end
|
56
98
|
|
57
99
|
protected
|
58
100
|
|
101
|
+
attr_reader :root
|
102
|
+
|
59
103
|
def valid_instance?(instance)
|
60
104
|
validate_instance(instance).none?
|
61
105
|
end
|
@@ -85,13 +129,13 @@ module JSONSchemer
|
|
85
129
|
ref = schema['$ref']
|
86
130
|
id = schema[id_keyword]
|
87
131
|
|
88
|
-
instance.parent_uri = join_uri(instance.parent_uri, id)
|
89
|
-
|
90
132
|
if ref
|
91
133
|
validate_ref(instance, ref, &block)
|
92
134
|
return
|
93
135
|
end
|
94
136
|
|
137
|
+
instance.base_uri = join_uri(instance.base_uri, id)
|
138
|
+
|
95
139
|
if format? && custom_format?(format)
|
96
140
|
validate_custom_format(instance, formats.fetch(format), &block)
|
97
141
|
end
|
@@ -101,7 +145,7 @@ module JSONSchemer
|
|
101
145
|
if keywords
|
102
146
|
keywords.each do |keyword, callable|
|
103
147
|
if schema.key?(keyword)
|
104
|
-
result = callable.call(data, schema, instance.
|
148
|
+
result = callable.call(data, schema, instance.data_pointer)
|
105
149
|
if result.is_a?(Array)
|
106
150
|
result.each(&block)
|
107
151
|
elsif !result
|
@@ -119,7 +163,8 @@ module JSONSchemer
|
|
119
163
|
subinstance = instance.merge(
|
120
164
|
schema: subschema,
|
121
165
|
schema_pointer: "#{instance.schema_pointer}/allOf/#{index}",
|
122
|
-
|
166
|
+
before_property_validation: false,
|
167
|
+
after_property_validation: false
|
123
168
|
)
|
124
169
|
validate_instance(subinstance, &block)
|
125
170
|
end
|
@@ -130,7 +175,8 @@ module JSONSchemer
|
|
130
175
|
subinstance = instance.merge(
|
131
176
|
schema: subschema,
|
132
177
|
schema_pointer: "#{instance.schema_pointer}/anyOf/#{index}",
|
133
|
-
|
178
|
+
before_property_validation: false,
|
179
|
+
after_property_validation: false
|
134
180
|
)
|
135
181
|
validate_instance(subinstance)
|
136
182
|
end
|
@@ -142,7 +188,8 @@ module JSONSchemer
|
|
142
188
|
subinstance = instance.merge(
|
143
189
|
schema: subschema,
|
144
190
|
schema_pointer: "#{instance.schema_pointer}/oneOf/#{index}",
|
145
|
-
|
191
|
+
before_property_validation: false,
|
192
|
+
after_property_validation: false
|
146
193
|
)
|
147
194
|
validate_instance(subinstance)
|
148
195
|
end
|
@@ -158,14 +205,15 @@ module JSONSchemer
|
|
158
205
|
subinstance = instance.merge(
|
159
206
|
schema: not_schema,
|
160
207
|
schema_pointer: "#{instance.schema_pointer}/not",
|
161
|
-
|
208
|
+
before_property_validation: false,
|
209
|
+
after_property_validation: false
|
162
210
|
)
|
163
211
|
yield error(subinstance, 'not') if valid_instance?(subinstance)
|
164
212
|
end
|
165
213
|
|
166
|
-
if if_schema && valid_instance?(instance.merge(schema: if_schema,
|
214
|
+
if if_schema && valid_instance?(instance.merge(schema: if_schema, before_property_validation: false, after_property_validation: false))
|
167
215
|
validate_instance(instance.merge(schema: then_schema, schema_pointer: "#{instance.schema_pointer}/then"), &block) unless then_schema.nil?
|
168
|
-
elsif
|
216
|
+
elsif schema.key?('if')
|
169
217
|
validate_instance(instance.merge(schema: else_schema, schema_pointer: "#{instance.schema_pointer}/else"), &block) unless else_schema.nil?
|
170
218
|
end
|
171
219
|
|
@@ -189,7 +237,7 @@ module JSONSchemer
|
|
189
237
|
|
190
238
|
private
|
191
239
|
|
192
|
-
attr_reader :
|
240
|
+
attr_reader :formats, :keywords, :ref_resolver, :regexp_resolver
|
193
241
|
|
194
242
|
def id_keyword
|
195
243
|
ID_KEYWORD
|
@@ -207,13 +255,16 @@ module JSONSchemer
|
|
207
255
|
!custom_format?(format) && supported_format?(format)
|
208
256
|
end
|
209
257
|
|
210
|
-
def child(schema)
|
258
|
+
def child(schema, base_uri:)
|
211
259
|
JSONSchemer.schema(
|
212
260
|
schema,
|
261
|
+
default_schema_class: self.class,
|
262
|
+
base_uri: base_uri,
|
213
263
|
format: format?,
|
214
264
|
formats: formats,
|
215
265
|
keywords: keywords,
|
216
|
-
ref_resolver: ref_resolver
|
266
|
+
ref_resolver: ref_resolver,
|
267
|
+
regexp_resolver: regexp_resolver
|
217
268
|
)
|
218
269
|
end
|
219
270
|
|
@@ -265,50 +316,38 @@ module JSONSchemer
|
|
265
316
|
end
|
266
317
|
|
267
318
|
def validate_ref(instance, ref, &block)
|
268
|
-
|
269
|
-
schema_pointer = ref.slice(1..-1)
|
270
|
-
if valid_json_pointer?(schema_pointer)
|
271
|
-
ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(schema_pointer))
|
272
|
-
subinstance = instance.merge(
|
273
|
-
schema: ref_pointer.eval(root),
|
274
|
-
schema_pointer: schema_pointer,
|
275
|
-
parent_uri: (pointer_uri(root, ref_pointer) || instance.parent_uri)
|
276
|
-
)
|
277
|
-
validate_instance(subinstance, &block)
|
278
|
-
return
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
ref_uri = join_uri(instance.parent_uri, ref)
|
319
|
+
ref_uri = join_uri(instance.base_uri, ref)
|
283
320
|
|
321
|
+
ref_uri_pointer = ''
|
284
322
|
if valid_json_pointer?(ref_uri.fragment)
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
parent_uri: (pointer_uri(ref_root, ref_pointer) || ref_uri)
|
292
|
-
)
|
293
|
-
ref_object.validate_instance(subinstance, &block)
|
294
|
-
elsif id = ids[ref_uri.to_s]
|
295
|
-
subinstance = instance.merge(
|
296
|
-
schema: id.fetch(:schema),
|
297
|
-
schema_pointer: id.fetch(:pointer),
|
298
|
-
parent_uri: ref_uri
|
299
|
-
)
|
300
|
-
validate_instance(subinstance, &block)
|
323
|
+
ref_uri_pointer = ref_uri.fragment
|
324
|
+
ref_uri.fragment = nil
|
325
|
+
end
|
326
|
+
|
327
|
+
ref_object = if ids.key?(ref_uri) || ref_uri.to_s == @base_uri.to_s
|
328
|
+
self
|
301
329
|
else
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
)
|
310
|
-
|
330
|
+
child(resolve_ref(ref_uri), base_uri: ref_uri)
|
331
|
+
end
|
332
|
+
|
333
|
+
ref_schema, ref_schema_pointer = ref_object.ids[ref_uri] || [ref_object.root, '']
|
334
|
+
|
335
|
+
ref_uri_pointer_parts = Hana::Pointer.parse(URI.decode_www_form_component(ref_uri_pointer))
|
336
|
+
schema, base_uri = ref_uri_pointer_parts.reduce([ref_schema, ref_uri]) do |(obj, uri), token|
|
337
|
+
if obj.is_a?(Array)
|
338
|
+
[obj.fetch(token.to_i), uri]
|
339
|
+
else
|
340
|
+
[obj.fetch(token), join_uri(uri, obj[id_keyword])]
|
341
|
+
end
|
311
342
|
end
|
343
|
+
|
344
|
+
subinstance = instance.merge(
|
345
|
+
schema: schema,
|
346
|
+
schema_pointer: "#{ref_schema_pointer}#{ref_uri_pointer}",
|
347
|
+
base_uri: base_uri
|
348
|
+
)
|
349
|
+
|
350
|
+
ref_object.validate_instance(subinstance, &block)
|
312
351
|
end
|
313
352
|
|
314
353
|
def validate_custom_format(instance, custom_format)
|
@@ -340,8 +379,7 @@ module JSONSchemer
|
|
340
379
|
validate_exclusive_minimum(instance, exclusive_minimum, minimum, &block) if exclusive_minimum
|
341
380
|
|
342
381
|
if multiple_of
|
343
|
-
|
344
|
-
yield error(instance, 'multipleOf') unless quotient.floor == quotient
|
382
|
+
yield error(instance, 'multipleOf') unless BigDecimal(data.to_s).modulo(multiple_of).zero?
|
345
383
|
end
|
346
384
|
end
|
347
385
|
|
@@ -384,7 +422,7 @@ module JSONSchemer
|
|
384
422
|
|
385
423
|
yield error(instance, 'maxLength') if max_length && data.size > max_length
|
386
424
|
yield error(instance, 'minLength') if min_length && data.size < min_length
|
387
|
-
yield error(instance, 'pattern') if pattern &&
|
425
|
+
yield error(instance, 'pattern') if pattern && !resolve_regexp(pattern).match?(data)
|
388
426
|
yield error(instance, 'format') if format? && spec_format?(format) && !valid_spec_format?(data, format)
|
389
427
|
|
390
428
|
if content_encoding || content_media_type
|
@@ -487,10 +525,10 @@ module JSONSchemer
|
|
487
525
|
dependencies = schema['dependencies']
|
488
526
|
property_names = schema['propertyNames']
|
489
527
|
|
490
|
-
if instance.
|
528
|
+
if instance.before_property_validation && properties
|
491
529
|
properties.each do |property, property_schema|
|
492
|
-
|
493
|
-
data
|
530
|
+
instance.before_property_validation.each do |hook|
|
531
|
+
hook.call(data, property, property_schema, schema)
|
494
532
|
end
|
495
533
|
end
|
496
534
|
end
|
@@ -499,7 +537,8 @@ module JSONSchemer
|
|
499
537
|
dependencies.each do |key, value|
|
500
538
|
next unless data.key?(key)
|
501
539
|
subschema = value.is_a?(Array) ? { 'required' => value } : value
|
502
|
-
|
540
|
+
escaped_key = escape_json_pointer_token(key)
|
541
|
+
subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{escaped_key}")
|
503
542
|
validate_instance(subinstance, &block)
|
504
543
|
end
|
505
544
|
end
|
@@ -513,6 +552,8 @@ module JSONSchemer
|
|
513
552
|
|
514
553
|
regex_pattern_properties = nil
|
515
554
|
data.each do |key, value|
|
555
|
+
escaped_key = escape_json_pointer_token(key)
|
556
|
+
|
516
557
|
unless property_names.nil?
|
517
558
|
subinstance = instance.merge(
|
518
559
|
data: key,
|
@@ -527,9 +568,9 @@ module JSONSchemer
|
|
527
568
|
if properties && properties.key?(key)
|
528
569
|
subinstance = instance.merge(
|
529
570
|
data: value,
|
530
|
-
data_pointer: "#{instance.data_pointer}/#{
|
571
|
+
data_pointer: "#{instance.data_pointer}/#{escaped_key}",
|
531
572
|
schema: properties[key],
|
532
|
-
schema_pointer: "#{instance.schema_pointer}/properties/#{
|
573
|
+
schema_pointer: "#{instance.schema_pointer}/properties/#{escaped_key}"
|
533
574
|
)
|
534
575
|
validate_instance(subinstance, &block)
|
535
576
|
matched_key = true
|
@@ -537,15 +578,16 @@ module JSONSchemer
|
|
537
578
|
|
538
579
|
if pattern_properties
|
539
580
|
regex_pattern_properties ||= pattern_properties.map do |pattern, property_schema|
|
540
|
-
[pattern,
|
581
|
+
[pattern, resolve_regexp(pattern), property_schema]
|
541
582
|
end
|
542
583
|
regex_pattern_properties.each do |pattern, regex, property_schema|
|
584
|
+
escaped_pattern = escape_json_pointer_token(pattern)
|
543
585
|
if regex.match?(key)
|
544
586
|
subinstance = instance.merge(
|
545
587
|
data: value,
|
546
|
-
data_pointer: "#{instance.data_pointer}/#{
|
588
|
+
data_pointer: "#{instance.data_pointer}/#{escaped_key}",
|
547
589
|
schema: property_schema,
|
548
|
-
schema_pointer: "#{instance.schema_pointer}/patternProperties/#{
|
590
|
+
schema_pointer: "#{instance.schema_pointer}/patternProperties/#{escaped_pattern}"
|
549
591
|
)
|
550
592
|
validate_instance(subinstance, &block)
|
551
593
|
matched_key = true
|
@@ -558,34 +600,36 @@ module JSONSchemer
|
|
558
600
|
unless additional_properties.nil?
|
559
601
|
subinstance = instance.merge(
|
560
602
|
data: value,
|
561
|
-
data_pointer: "#{instance.data_pointer}/#{
|
603
|
+
data_pointer: "#{instance.data_pointer}/#{escaped_key}",
|
562
604
|
schema: additional_properties,
|
563
605
|
schema_pointer: "#{instance.schema_pointer}/additionalProperties"
|
564
606
|
)
|
565
607
|
validate_instance(subinstance, &block)
|
566
608
|
end
|
567
609
|
end
|
610
|
+
|
611
|
+
if instance.after_property_validation && properties
|
612
|
+
properties.each do |property, property_schema|
|
613
|
+
instance.after_property_validation.each do |hook|
|
614
|
+
hook.call(data, property, property_schema, schema)
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
568
618
|
end
|
569
619
|
|
570
620
|
def safe_strict_decode64(data)
|
571
621
|
Base64.strict_decode64(data)
|
572
|
-
rescue ArgumentError
|
573
|
-
raise e unless e.message == 'invalid base64'
|
622
|
+
rescue ArgumentError
|
574
623
|
nil
|
575
624
|
end
|
576
625
|
|
577
|
-
def
|
578
|
-
|
579
|
-
@ecma_262_regex[pattern] ||= Regexp.new(
|
580
|
-
Regexp::Scanner.scan(pattern).map do |type, token, text|
|
581
|
-
type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
|
582
|
-
end.join
|
583
|
-
)
|
626
|
+
def escape_json_pointer_token(token)
|
627
|
+
token.gsub(JSON_POINTER_TOKEN_ESCAPE_REGEX, JSON_POINTER_TOKEN_ESCAPE_CHARS)
|
584
628
|
end
|
585
629
|
|
586
630
|
def join_uri(a, b)
|
587
631
|
b = URI.parse(b) if b
|
588
|
-
if a && b && a.relative? && b.relative?
|
632
|
+
uri = if a && b && a.relative? && b.relative?
|
589
633
|
b
|
590
634
|
elsif a && b
|
591
635
|
URI.join(a, b)
|
@@ -594,34 +638,26 @@ module JSONSchemer
|
|
594
638
|
else
|
595
639
|
a
|
596
640
|
end
|
641
|
+
uri.fragment = nil if uri.is_a?(URI) && uri.fragment == ''
|
642
|
+
uri
|
597
643
|
end
|
598
644
|
|
599
|
-
def
|
600
|
-
uri_parts = nil
|
601
|
-
pointer.reduce(schema) do |obj, token|
|
602
|
-
next obj.fetch(token.to_i) if obj.is_a?(Array)
|
603
|
-
if obj_id = obj[id_keyword]
|
604
|
-
uri_parts ||= []
|
605
|
-
uri_parts << obj_id
|
606
|
-
end
|
607
|
-
obj.fetch(token)
|
608
|
-
end
|
609
|
-
uri_parts ? URI.join(*uri_parts) : nil
|
610
|
-
end
|
611
|
-
|
612
|
-
def resolve_ids(schema, ids = {}, parent_uri = nil, pointer = '')
|
645
|
+
def resolve_ids(schema, ids = {}, base_uri = @base_uri, pointer = '')
|
613
646
|
if schema.is_a?(Array)
|
614
|
-
schema.each_with_index { |subschema, index| resolve_ids(subschema, ids,
|
647
|
+
schema.each_with_index { |subschema, index| resolve_ids(subschema, ids, base_uri, "#{pointer}/#{index}") }
|
615
648
|
elsif schema.is_a?(Hash)
|
616
|
-
uri = join_uri(
|
649
|
+
uri = join_uri(base_uri, schema[id_keyword])
|
617
650
|
schema.each do |key, value|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
}
|
651
|
+
case key
|
652
|
+
when id_keyword
|
653
|
+
ids[uri] ||= [schema, pointer]
|
654
|
+
when 'items', 'allOf', 'anyOf', 'oneOf', 'additionalItems', 'contains', 'additionalProperties', 'propertyNames', 'if', 'then', 'else', 'not'
|
655
|
+
resolve_ids(value, ids, uri, "#{pointer}/#{key}")
|
656
|
+
when 'properties', 'patternProperties', 'definitions', 'dependencies'
|
657
|
+
value.each do |subkey, subvalue|
|
658
|
+
resolve_ids(subvalue, ids, uri, "#{pointer}/#{key}/#{subkey}")
|
659
|
+
end
|
623
660
|
end
|
624
|
-
resolve_ids(value, ids, uri, "#{pointer}/#{key}")
|
625
661
|
end
|
626
662
|
end
|
627
663
|
ids
|
@@ -630,6 +666,10 @@ module JSONSchemer
|
|
630
666
|
def resolve_ref(uri)
|
631
667
|
ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s)
|
632
668
|
end
|
669
|
+
|
670
|
+
def resolve_regexp(pattern)
|
671
|
+
regexp_resolver.call(pattern) || raise(InvalidRegexpResolution, pattern)
|
672
|
+
end
|
633
673
|
end
|
634
674
|
end
|
635
675
|
end
|