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.
@@ -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
- INVALID_QUERY_REGEX = /[[:space:]]/.freeze
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, :v4)
35
+ valid_ip?(data, Socket::AF_INET)
32
36
  when 'ipv6'
33
- valid_ip?(data, :v6)
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
- EcmaReValidator.valid?(data)
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
- DateTime.rfc3339(data)
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 => e
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
- end
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, type)
77
- ip_address = IPAddr.new(data)
78
- type == :v4 ? ip_address.ipv4? : ip_address.ipv6?
79
- rescue IPAddr::InvalidAddressError
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
- URI.escape(data, /[^[[:ascii:]]]/)
105
- end
106
-
107
- def valid_uri_template?(data)
108
- URITemplate.new(data)
109
- true
110
- rescue URITemplate::Invalid
111
- false
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, :parent_uri, :insert_property_defaults) do
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
- parent_uri: self.parent_uri,
14
- insert_property_defaults: self.insert_property_defaults
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, parent_uri, insert_property_defaults)
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
- RUBY_REGEX_ANCHORS_TO_ECMA_262 = {
26
- :bos => 'A',
27
- :eos => 'z',
28
- :bol => '\A',
29
- :eol => '\z'
30
- }.freeze
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
- @insert_property_defaults = insert_property_defaults
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' ? CachedRefResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
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, '', nil, !!@insert_property_defaults))
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, '', nil, !!@insert_property_defaults))
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.pointer)
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
- insert_property_defaults: false
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
- insert_property_defaults: false
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
- insert_property_defaults: false
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
- insert_property_defaults: false
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, insert_property_defaults: false))
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 if_schema
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 :root, :formats, :keywords, :ref_resolver
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
- if ref.start_with?('#')
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
- ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(ref_uri.fragment))
286
- ref_root = resolve_ref(ref_uri)
287
- ref_object = child(ref_root)
288
- subinstance = instance.merge(
289
- schema: ref_pointer.eval(ref_root),
290
- schema_pointer: ref_uri.fragment,
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
- ref_root = resolve_ref(ref_uri)
303
- ref_object = child(ref_root)
304
- id = ref_object.ids[ref_uri.to_s] || { schema: ref_root, pointer: '' }
305
- subinstance = instance.merge(
306
- schema: id.fetch(:schema),
307
- schema_pointer: id.fetch(:pointer),
308
- parent_uri: ref_uri
309
- )
310
- ref_object.validate_instance(subinstance, &block)
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
- quotient = data / multiple_of.to_f
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 && ecma_262_regex(pattern) !~ data
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.insert_property_defaults && properties
528
+ if instance.before_property_validation && properties
491
529
  properties.each do |property, property_schema|
492
- if !data.key?(property) && property_schema.is_a?(Hash) && property_schema.key?('default')
493
- data[property] = property_schema.fetch('default').clone
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
- subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{key}")
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}/#{key}",
571
+ data_pointer: "#{instance.data_pointer}/#{escaped_key}",
531
572
  schema: properties[key],
532
- schema_pointer: "#{instance.schema_pointer}/properties/#{key}"
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, ecma_262_regex(pattern), property_schema]
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}/#{key}",
588
+ data_pointer: "#{instance.data_pointer}/#{escaped_key}",
547
589
  schema: property_schema,
548
- schema_pointer: "#{instance.schema_pointer}/patternProperties/#{pattern}"
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}/#{key}",
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 => e
573
- raise e unless e.message == 'invalid base64'
622
+ rescue ArgumentError
574
623
  nil
575
624
  end
576
625
 
577
- def ecma_262_regex(pattern)
578
- @ecma_262_regex ||= {}
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 pointer_uri(schema, pointer)
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, parent_uri, "#{pointer}/#{index}") }
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(parent_uri, schema[id_keyword])
649
+ uri = join_uri(base_uri, schema[id_keyword])
617
650
  schema.each do |key, value|
618
- if key == id_keyword && uri != parent_uri
619
- ids[uri.to_s] = {
620
- schema: schema,
621
- pointer: pointer
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