json_schemer 0.2.15 → 1.0.1
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 +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
|