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.
@@ -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