json_schemer 0.2.24 → 1.0.0

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.
@@ -4,40 +4,27 @@ module JSONSchemer
4
4
  class Base
5
5
  include Format
6
6
 
7
- Instance = Struct.new(:data, :data_pointer, :schema, :schema_pointer, :parent_uri, :before_property_validation, :after_property_validation) 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,
13
+ base_uri: self.base_uri,
14
14
  before_property_validation: self.before_property_validation,
15
15
  after_property_validation: self.after_property_validation
16
16
  )
17
- self.class.new(data, data_pointer, schema, schema_pointer, parent_uri, before_property_validation, after_property_validation)
17
+ self.class.new(data, data_pointer, schema, schema_pointer, base_uri, before_property_validation, after_property_validation)
18
18
  end
19
19
  end
20
20
 
21
21
  ID_KEYWORD = '$id'
22
22
  DEFAULT_REF_RESOLVER = proc { |uri| raise UnknownRef, uri.to_s }
23
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)) }
24
26
  BOOLEANS = Set[true, false].freeze
25
27
 
26
- RUBY_REGEX_ANCHORS_TO_ECMA_262 = {
27
- :bos => 'A',
28
- :eos => 'z',
29
- :bol => '\A',
30
- :eol => '\z'
31
- }.freeze
32
-
33
- ECMA_262_REGEXP_RESOLVER = proc do |pattern|
34
- Regexp.new(
35
- Regexp::Scanner.scan(pattern).map do |type, token, text|
36
- type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
37
- end.join
38
- )
39
- end
40
-
41
28
  INSERT_DEFAULT_PROPERTY = proc do |data, property, property_schema, _parent|
42
29
  if !data.key?(property) && property_schema.is_a?(Hash) && property_schema.key?('default')
43
30
  data[property] = property_schema.fetch('default').clone
@@ -47,8 +34,23 @@ module JSONSchemer
47
34
  JSON_POINTER_TOKEN_ESCAPE_CHARS = { '~' => '~0', '/' => '~1' }
48
35
  JSON_POINTER_TOKEN_ESCAPE_REGEXP = Regexp.union(JSON_POINTER_TOKEN_ESCAPE_CHARS.keys)
49
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
50
+
50
51
  def initialize(
51
52
  schema,
53
+ base_uri: nil,
52
54
  format: true,
53
55
  insert_property_defaults: false,
54
56
  before_property_validation: nil,
@@ -56,10 +58,11 @@ module JSONSchemer
56
58
  formats: nil,
57
59
  keywords: nil,
58
60
  ref_resolver: DEFAULT_REF_RESOLVER,
59
- regexp_resolver: 'ecma'
61
+ regexp_resolver: 'ruby'
60
62
  )
61
63
  raise InvalidSymbolKey, 'schemas must use string keys' if schema.is_a?(Hash) && !schema.empty? && !schema.first.first.is_a?(String)
62
64
  @root = schema
65
+ @base_uri = base_uri
63
66
  @format = format
64
67
  @before_property_validation = [*before_property_validation]
65
68
  @before_property_validation.unshift(INSERT_DEFAULT_PROPERTY) if insert_property_defaults
@@ -69,24 +72,34 @@ module JSONSchemer
69
72
  @ref_resolver = ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
70
73
  @regexp_resolver = case regexp_resolver
71
74
  when 'ecma'
72
- CachedResolver.new(&ECMA_262_REGEXP_RESOLVER)
75
+ CachedResolver.new(&ECMA_REGEXP_RESOLVER)
73
76
  when 'ruby'
74
- CachedResolver.new(&Regexp.method(:new))
77
+ CachedResolver.new(&RUBY_REGEXP_RESOLVER)
75
78
  else
76
79
  regexp_resolver
77
80
  end
78
81
  end
79
82
 
80
83
  def valid?(data)
81
- valid_instance?(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation))
84
+ valid_instance?(Instance.new(data, '', root, '', @base_uri, @before_property_validation, @after_property_validation))
82
85
  end
83
86
 
84
87
  def validate(data)
85
- validate_instance(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation))
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)
86
97
  end
87
98
 
88
99
  protected
89
100
 
101
+ attr_reader :root
102
+
90
103
  def valid_instance?(instance)
91
104
  validate_instance(instance).none?
92
105
  end
@@ -116,13 +129,13 @@ module JSONSchemer
116
129
  ref = schema['$ref']
117
130
  id = schema[id_keyword]
118
131
 
119
- instance.parent_uri = join_uri(instance.parent_uri, id)
120
-
121
132
  if ref
122
133
  validate_ref(instance, ref, &block)
123
134
  return
124
135
  end
125
136
 
137
+ instance.base_uri = join_uri(instance.base_uri, id)
138
+
126
139
  if format? && custom_format?(format)
127
140
  validate_custom_format(instance, formats.fetch(format), &block)
128
141
  end
@@ -200,7 +213,7 @@ module JSONSchemer
200
213
 
201
214
  if if_schema && valid_instance?(instance.merge(schema: if_schema, before_property_validation: false, after_property_validation: false))
202
215
  validate_instance(instance.merge(schema: then_schema, schema_pointer: "#{instance.schema_pointer}/then"), &block) unless then_schema.nil?
203
- elsif if_schema
216
+ elsif schema.key?('if')
204
217
  validate_instance(instance.merge(schema: else_schema, schema_pointer: "#{instance.schema_pointer}/else"), &block) unless else_schema.nil?
205
218
  end
206
219
 
@@ -224,7 +237,7 @@ module JSONSchemer
224
237
 
225
238
  private
226
239
 
227
- attr_reader :root, :formats, :keywords, :ref_resolver, :regexp_resolver
240
+ attr_reader :formats, :keywords, :ref_resolver, :regexp_resolver
228
241
 
229
242
  def id_keyword
230
243
  ID_KEYWORD
@@ -242,9 +255,11 @@ module JSONSchemer
242
255
  !custom_format?(format) && supported_format?(format)
243
256
  end
244
257
 
245
- def child(schema)
258
+ def child(schema, base_uri:)
246
259
  JSONSchemer.schema(
247
260
  schema,
261
+ default_schema_class: self.class,
262
+ base_uri: base_uri,
248
263
  format: format?,
249
264
  formats: formats,
250
265
  keywords: keywords,
@@ -301,50 +316,38 @@ module JSONSchemer
301
316
  end
302
317
 
303
318
  def validate_ref(instance, ref, &block)
304
- if ref.start_with?('#')
305
- schema_pointer = ref.slice(1..-1)
306
- if valid_json_pointer?(schema_pointer)
307
- ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(schema_pointer))
308
- subinstance = instance.merge(
309
- schema: ref_pointer.eval(root),
310
- schema_pointer: schema_pointer,
311
- parent_uri: (pointer_uri(root, ref_pointer) || instance.parent_uri)
312
- )
313
- validate_instance(subinstance, &block)
314
- return
315
- end
316
- end
317
-
318
- ref_uri = join_uri(instance.parent_uri, ref)
319
+ ref_uri = join_uri(instance.base_uri, ref)
319
320
 
321
+ ref_uri_pointer = ''
320
322
  if valid_json_pointer?(ref_uri.fragment)
321
- ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(ref_uri.fragment))
322
- ref_root = resolve_ref(ref_uri)
323
- ref_object = child(ref_root)
324
- subinstance = instance.merge(
325
- schema: ref_pointer.eval(ref_root),
326
- schema_pointer: ref_uri.fragment,
327
- parent_uri: (pointer_uri(ref_root, ref_pointer) || ref_uri)
328
- )
329
- ref_object.validate_instance(subinstance, &block)
330
- elsif id = ids[ref_uri.to_s]
331
- subinstance = instance.merge(
332
- schema: id.fetch(:schema),
333
- schema_pointer: id.fetch(:pointer),
334
- parent_uri: ref_uri
335
- )
336
- 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
337
329
  else
338
- ref_root = resolve_ref(ref_uri)
339
- ref_object = child(ref_root)
340
- id = ref_object.ids[ref_uri.to_s] || { schema: ref_root, pointer: '' }
341
- subinstance = instance.merge(
342
- schema: id.fetch(:schema),
343
- schema_pointer: id.fetch(:pointer),
344
- parent_uri: ref_uri
345
- )
346
- 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
347
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)
348
351
  end
349
352
 
350
353
  def validate_custom_format(instance, custom_format)
@@ -616,8 +619,7 @@ module JSONSchemer
616
619
 
617
620
  def safe_strict_decode64(data)
618
621
  Base64.strict_decode64(data)
619
- rescue ArgumentError => e
620
- raise e unless e.message == 'invalid base64'
622
+ rescue ArgumentError
621
623
  nil
622
624
  end
623
625
 
@@ -627,7 +629,7 @@ module JSONSchemer
627
629
 
628
630
  def join_uri(a, b)
629
631
  b = URI.parse(b) if b
630
- if a && b && a.relative? && b.relative?
632
+ uri = if a && b && a.relative? && b.relative?
631
633
  b
632
634
  elsif a && b
633
635
  URI.join(a, b)
@@ -636,34 +638,26 @@ module JSONSchemer
636
638
  else
637
639
  a
638
640
  end
641
+ uri.fragment = nil if uri.is_a?(URI) && uri.fragment == ''
642
+ uri
639
643
  end
640
644
 
641
- def pointer_uri(schema, pointer)
642
- uri_parts = nil
643
- pointer.reduce(schema) do |obj, token|
644
- next obj.fetch(token.to_i) if obj.is_a?(Array)
645
- if obj_id = obj[id_keyword]
646
- uri_parts ||= []
647
- uri_parts << obj_id
648
- end
649
- obj.fetch(token)
650
- end
651
- uri_parts ? URI.join(*uri_parts) : nil
652
- end
653
-
654
- def resolve_ids(schema, ids = {}, parent_uri = nil, pointer = '')
645
+ def resolve_ids(schema, ids = {}, base_uri = @base_uri, pointer = '')
655
646
  if schema.is_a?(Array)
656
- 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}") }
657
648
  elsif schema.is_a?(Hash)
658
- uri = join_uri(parent_uri, schema[id_keyword])
649
+ uri = join_uri(base_uri, schema[id_keyword])
659
650
  schema.each do |key, value|
660
- if key == id_keyword && uri != parent_uri
661
- ids[uri.to_s] = {
662
- schema: schema,
663
- pointer: pointer
664
- }
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
665
660
  end
666
- resolve_ids(value, ids, uri, "#{pointer}/#{key}")
667
661
  end
668
662
  end
669
663
  ids
@@ -0,0 +1,149 @@
1
+ {
2
+ "id": "http://json-schema.org/draft-04/schema#",
3
+ "$schema": "http://json-schema.org/draft-04/schema#",
4
+ "description": "Core schema meta-schema",
5
+ "definitions": {
6
+ "schemaArray": {
7
+ "type": "array",
8
+ "minItems": 1,
9
+ "items": { "$ref": "#" }
10
+ },
11
+ "positiveInteger": {
12
+ "type": "integer",
13
+ "minimum": 0
14
+ },
15
+ "positiveIntegerDefault0": {
16
+ "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
17
+ },
18
+ "simpleTypes": {
19
+ "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
20
+ },
21
+ "stringArray": {
22
+ "type": "array",
23
+ "items": { "type": "string" },
24
+ "minItems": 1,
25
+ "uniqueItems": true
26
+ }
27
+ },
28
+ "type": "object",
29
+ "properties": {
30
+ "id": {
31
+ "type": "string"
32
+ },
33
+ "$schema": {
34
+ "type": "string"
35
+ },
36
+ "title": {
37
+ "type": "string"
38
+ },
39
+ "description": {
40
+ "type": "string"
41
+ },
42
+ "default": {},
43
+ "multipleOf": {
44
+ "type": "number",
45
+ "minimum": 0,
46
+ "exclusiveMinimum": true
47
+ },
48
+ "maximum": {
49
+ "type": "number"
50
+ },
51
+ "exclusiveMaximum": {
52
+ "type": "boolean",
53
+ "default": false
54
+ },
55
+ "minimum": {
56
+ "type": "number"
57
+ },
58
+ "exclusiveMinimum": {
59
+ "type": "boolean",
60
+ "default": false
61
+ },
62
+ "maxLength": { "$ref": "#/definitions/positiveInteger" },
63
+ "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
64
+ "pattern": {
65
+ "type": "string",
66
+ "format": "regex"
67
+ },
68
+ "additionalItems": {
69
+ "anyOf": [
70
+ { "type": "boolean" },
71
+ { "$ref": "#" }
72
+ ],
73
+ "default": {}
74
+ },
75
+ "items": {
76
+ "anyOf": [
77
+ { "$ref": "#" },
78
+ { "$ref": "#/definitions/schemaArray" }
79
+ ],
80
+ "default": {}
81
+ },
82
+ "maxItems": { "$ref": "#/definitions/positiveInteger" },
83
+ "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
84
+ "uniqueItems": {
85
+ "type": "boolean",
86
+ "default": false
87
+ },
88
+ "maxProperties": { "$ref": "#/definitions/positiveInteger" },
89
+ "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
90
+ "required": { "$ref": "#/definitions/stringArray" },
91
+ "additionalProperties": {
92
+ "anyOf": [
93
+ { "type": "boolean" },
94
+ { "$ref": "#" }
95
+ ],
96
+ "default": {}
97
+ },
98
+ "definitions": {
99
+ "type": "object",
100
+ "additionalProperties": { "$ref": "#" },
101
+ "default": {}
102
+ },
103
+ "properties": {
104
+ "type": "object",
105
+ "additionalProperties": { "$ref": "#" },
106
+ "default": {}
107
+ },
108
+ "patternProperties": {
109
+ "type": "object",
110
+ "additionalProperties": { "$ref": "#" },
111
+ "default": {}
112
+ },
113
+ "dependencies": {
114
+ "type": "object",
115
+ "additionalProperties": {
116
+ "anyOf": [
117
+ { "$ref": "#" },
118
+ { "$ref": "#/definitions/stringArray" }
119
+ ]
120
+ }
121
+ },
122
+ "enum": {
123
+ "type": "array",
124
+ "minItems": 1,
125
+ "uniqueItems": true
126
+ },
127
+ "type": {
128
+ "anyOf": [
129
+ { "$ref": "#/definitions/simpleTypes" },
130
+ {
131
+ "type": "array",
132
+ "items": { "$ref": "#/definitions/simpleTypes" },
133
+ "minItems": 1,
134
+ "uniqueItems": true
135
+ }
136
+ ]
137
+ },
138
+ "format": { "type": "string" },
139
+ "allOf": { "$ref": "#/definitions/schemaArray" },
140
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
141
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
142
+ "not": { "$ref": "#" }
143
+ },
144
+ "dependencies": {
145
+ "exclusiveMaximum": [ "maximum" ],
146
+ "exclusiveMinimum": [ "minimum" ]
147
+ },
148
+ "default": {}
149
+ }
@@ -0,0 +1,155 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-06/schema#",
3
+ "$id": "http://json-schema.org/draft-06/schema#",
4
+ "title": "Core schema meta-schema",
5
+ "definitions": {
6
+ "schemaArray": {
7
+ "type": "array",
8
+ "minItems": 1,
9
+ "items": { "$ref": "#" }
10
+ },
11
+ "nonNegativeInteger": {
12
+ "type": "integer",
13
+ "minimum": 0
14
+ },
15
+ "nonNegativeIntegerDefault0": {
16
+ "allOf": [
17
+ { "$ref": "#/definitions/nonNegativeInteger" },
18
+ { "default": 0 }
19
+ ]
20
+ },
21
+ "simpleTypes": {
22
+ "enum": [
23
+ "array",
24
+ "boolean",
25
+ "integer",
26
+ "null",
27
+ "number",
28
+ "object",
29
+ "string"
30
+ ]
31
+ },
32
+ "stringArray": {
33
+ "type": "array",
34
+ "items": { "type": "string" },
35
+ "uniqueItems": true,
36
+ "default": []
37
+ }
38
+ },
39
+ "type": ["object", "boolean"],
40
+ "properties": {
41
+ "$id": {
42
+ "type": "string",
43
+ "format": "uri-reference"
44
+ },
45
+ "$schema": {
46
+ "type": "string",
47
+ "format": "uri"
48
+ },
49
+ "$ref": {
50
+ "type": "string",
51
+ "format": "uri-reference"
52
+ },
53
+ "title": {
54
+ "type": "string"
55
+ },
56
+ "description": {
57
+ "type": "string"
58
+ },
59
+ "default": {},
60
+ "examples": {
61
+ "type": "array",
62
+ "items": {}
63
+ },
64
+ "multipleOf": {
65
+ "type": "number",
66
+ "exclusiveMinimum": 0
67
+ },
68
+ "maximum": {
69
+ "type": "number"
70
+ },
71
+ "exclusiveMaximum": {
72
+ "type": "number"
73
+ },
74
+ "minimum": {
75
+ "type": "number"
76
+ },
77
+ "exclusiveMinimum": {
78
+ "type": "number"
79
+ },
80
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
81
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
82
+ "pattern": {
83
+ "type": "string",
84
+ "format": "regex"
85
+ },
86
+ "additionalItems": { "$ref": "#" },
87
+ "items": {
88
+ "anyOf": [
89
+ { "$ref": "#" },
90
+ { "$ref": "#/definitions/schemaArray" }
91
+ ],
92
+ "default": {}
93
+ },
94
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
95
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
96
+ "uniqueItems": {
97
+ "type": "boolean",
98
+ "default": false
99
+ },
100
+ "contains": { "$ref": "#" },
101
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
102
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
103
+ "required": { "$ref": "#/definitions/stringArray" },
104
+ "additionalProperties": { "$ref": "#" },
105
+ "definitions": {
106
+ "type": "object",
107
+ "additionalProperties": { "$ref": "#" },
108
+ "default": {}
109
+ },
110
+ "properties": {
111
+ "type": "object",
112
+ "additionalProperties": { "$ref": "#" },
113
+ "default": {}
114
+ },
115
+ "patternProperties": {
116
+ "type": "object",
117
+ "additionalProperties": { "$ref": "#" },
118
+ "propertyNames": { "format": "regex" },
119
+ "default": {}
120
+ },
121
+ "dependencies": {
122
+ "type": "object",
123
+ "additionalProperties": {
124
+ "anyOf": [
125
+ { "$ref": "#" },
126
+ { "$ref": "#/definitions/stringArray" }
127
+ ]
128
+ }
129
+ },
130
+ "propertyNames": { "$ref": "#" },
131
+ "const": {},
132
+ "enum": {
133
+ "type": "array",
134
+ "minItems": 1,
135
+ "uniqueItems": true
136
+ },
137
+ "type": {
138
+ "anyOf": [
139
+ { "$ref": "#/definitions/simpleTypes" },
140
+ {
141
+ "type": "array",
142
+ "items": { "$ref": "#/definitions/simpleTypes" },
143
+ "minItems": 1,
144
+ "uniqueItems": true
145
+ }
146
+ ]
147
+ },
148
+ "format": { "type": "string" },
149
+ "allOf": { "$ref": "#/definitions/schemaArray" },
150
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
151
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
152
+ "not": { "$ref": "#" }
153
+ },
154
+ "default": {}
155
+ }