json_schemer 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,61 +1,161 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module JSONSchemer::Format
4
- # this is no good
5
- EMAIL_REGEX = /\A[^@\s]+@([\p{L}\d-]+\.)+[\p{L}\d\-]{2,}\z/i.freeze
6
- LABEL_REGEX_STRING = '\p{L}([\p{L}\p{N}\-]*[\p{L}\p{N}])?'
7
- HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze
8
- JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
9
- JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
10
- RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
11
-
12
- # https://github.com/ruby-rdf/rdf
13
-
14
- # IRI components
15
- UCSCHAR = Regexp.compile(<<-EOS.gsub(/\s+/, ''))
16
- [\\u00A0-\\uD7FF]|[\\uF900-\\uFDCF]|[\\uFDF0-\\uFFEF]|
17
- [\\u{10000}-\\u{1FFFD}]|[\\u{20000}-\\u{2FFFD}]|[\\u{30000}-\\u{3FFFD}]|
18
- [\\u{40000}-\\u{4FFFD}]|[\\u{50000}-\\u{5FFFD}]|[\\u{60000}-\\u{6FFFD}]|
19
- [\\u{70000}-\\u{7FFFD}]|[\\u{80000}-\\u{8FFFD}]|[\\u{90000}-\\u{9FFFD}]|
20
- [\\u{A0000}-\\u{AFFFD}]|[\\u{B0000}-\\u{BFFFD}]|[\\u{C0000}-\\u{CFFFD}]|
21
- [\\u{D0000}-\\u{DFFFD}]|[\\u{E1000}-\\u{EFFFD}]
22
- EOS
23
- IPRIVATE = Regexp.compile("[\\uE000-\\uF8FF]|[\\u{F0000}-\\u{FFFFD}]|[\\u100000-\\u10FFFD]").freeze
24
- SCHEME = Regexp.compile("[A-Za-z](?:[A-Za-z0-9+-\.])*").freeze
25
- PORT = Regexp.compile("[0-9]*").freeze
26
- IP_LITERAL = Regexp.compile("\\[[0-9A-Fa-f:\\.]*\\]").freeze # Simplified, no IPvFuture
27
- PCT_ENCODED = Regexp.compile("%[0-9A-Fa-f][0-9A-Fa-f]").freeze
28
- GEN_DELIMS = Regexp.compile("[:/\\?\\#\\[\\]@]").freeze
29
- SUB_DELIMS = Regexp.compile("[!\\$&'\\(\\)\\*\\+,;=]").freeze
30
- RESERVED = Regexp.compile("(?:#{GEN_DELIMS}|#{SUB_DELIMS})").freeze
31
- UNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]").freeze
32
-
33
- IUNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]|#{UCSCHAR}").freeze
34
-
35
- IPCHAR = Regexp.compile("(?:#{IUNRESERVED}|#{PCT_ENCODED}|#{SUB_DELIMS}|:|@)").freeze
36
-
37
- IQUERY = Regexp.compile("(?:#{IPCHAR}|#{IPRIVATE}|/|\\?)*").freeze
38
-
39
- IFRAGMENT = Regexp.compile("(?:#{IPCHAR}|/|\\?)*").freeze.freeze
40
-
41
- ISEGMENT = Regexp.compile("(?:#{IPCHAR})*").freeze
42
- ISEGMENT_NZ = Regexp.compile("(?:#{IPCHAR})+").freeze
43
- ISEGMENT_NZ_NC = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|@)+").freeze
44
-
45
- IPATH_ABEMPTY = Regexp.compile("(?:/#{ISEGMENT})*").freeze
46
- IPATH_ABSOLUTE = Regexp.compile("/(?:(?:#{ISEGMENT_NZ})(/#{ISEGMENT})*)?").freeze
47
- IPATH_NOSCHEME = Regexp.compile("(?:#{ISEGMENT_NZ_NC})(?:/#{ISEGMENT})*").freeze
48
- IPATH_ROOTLESS = Regexp.compile("(?:#{ISEGMENT_NZ})(?:/#{ISEGMENT})*").freeze
49
- IPATH_EMPTY = Regexp.compile("").freeze
50
-
51
- IREG_NAME = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS}))*").freeze
52
- IHOST = Regexp.compile("(?:#{IP_LITERAL})|(?:#{IREG_NAME})").freeze
53
- IUSERINFO = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|:)*").freeze
54
- IAUTHORITY = Regexp.compile("(?:#{IUSERINFO}@)?#{IHOST}(?::#{PORT})?").freeze
55
-
56
- IRELATIVE_PART = Regexp.compile("(?:(?://#{IAUTHORITY}(?:#{IPATH_ABEMPTY}))|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_NOSCHEME})|(?:#{IPATH_EMPTY}))").freeze
57
- IRELATIVE_REF = Regexp.compile("^#{IRELATIVE_PART}(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
58
-
59
- IHIER_PART = Regexp.compile("(?:(?://#{IAUTHORITY}#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_ROOTLESS})|(?:#{IPATH_EMPTY}))").freeze
60
- IRI = Regexp.compile("^#{SCHEME}:(?:#{IHIER_PART})(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
3
+ require 'ecma-re-validator'
4
+ require 'ipaddr'
5
+ require 'json'
6
+ require 'time'
7
+ require 'uri_template'
8
+
9
+ module JSONSchemer
10
+ module Format
11
+ # this is no good
12
+ EMAIL_REGEX = /\A[^@\s]+@([\p{L}\d-]+\.)+[\p{L}\d\-]{2,}\z/i.freeze
13
+ LABEL_REGEX_STRING = '\p{L}([\p{L}\p{N}\-]*[\p{L}\p{N}])?'
14
+ HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze
15
+ JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
16
+ JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
17
+ RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
18
+
19
+ # https://github.com/ruby-rdf/rdf
20
+
21
+ # IRI components
22
+ UCSCHAR = Regexp.compile(<<-EOS.gsub(/\s+/, ''))
23
+ [\\u00A0-\\uD7FF]|[\\uF900-\\uFDCF]|[\\uFDF0-\\uFFEF]|
24
+ [\\u{10000}-\\u{1FFFD}]|[\\u{20000}-\\u{2FFFD}]|[\\u{30000}-\\u{3FFFD}]|
25
+ [\\u{40000}-\\u{4FFFD}]|[\\u{50000}-\\u{5FFFD}]|[\\u{60000}-\\u{6FFFD}]|
26
+ [\\u{70000}-\\u{7FFFD}]|[\\u{80000}-\\u{8FFFD}]|[\\u{90000}-\\u{9FFFD}]|
27
+ [\\u{A0000}-\\u{AFFFD}]|[\\u{B0000}-\\u{BFFFD}]|[\\u{C0000}-\\u{CFFFD}]|
28
+ [\\u{D0000}-\\u{DFFFD}]|[\\u{E1000}-\\u{EFFFD}]
29
+ EOS
30
+ IPRIVATE = Regexp.compile("[\\uE000-\\uF8FF]|[\\u{F0000}-\\u{FFFFD}]|[\\u100000-\\u10FFFD]").freeze
31
+ SCHEME = Regexp.compile("[A-Za-z](?:[A-Za-z0-9+-\.])*").freeze
32
+ PORT = Regexp.compile("[0-9]*").freeze
33
+ IP_LITERAL = Regexp.compile("\\[[0-9A-Fa-f:\\.]*\\]").freeze # Simplified, no IPvFuture
34
+ PCT_ENCODED = Regexp.compile("%[0-9A-Fa-f][0-9A-Fa-f]").freeze
35
+ GEN_DELIMS = Regexp.compile("[:/\\?\\#\\[\\]@]").freeze
36
+ SUB_DELIMS = Regexp.compile("[!\\$&'\\(\\)\\*\\+,;=]").freeze
37
+ RESERVED = Regexp.compile("(?:#{GEN_DELIMS}|#{SUB_DELIMS})").freeze
38
+ UNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]").freeze
39
+
40
+ IUNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]|#{UCSCHAR}").freeze
41
+
42
+ IPCHAR = Regexp.compile("(?:#{IUNRESERVED}|#{PCT_ENCODED}|#{SUB_DELIMS}|:|@)").freeze
43
+
44
+ IQUERY = Regexp.compile("(?:#{IPCHAR}|#{IPRIVATE}|/|\\?)*").freeze
45
+
46
+ IFRAGMENT = Regexp.compile("(?:#{IPCHAR}|/|\\?)*").freeze.freeze
47
+
48
+ ISEGMENT = Regexp.compile("(?:#{IPCHAR})*").freeze
49
+ ISEGMENT_NZ = Regexp.compile("(?:#{IPCHAR})+").freeze
50
+ ISEGMENT_NZ_NC = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|@)+").freeze
51
+
52
+ IPATH_ABEMPTY = Regexp.compile("(?:/#{ISEGMENT})*").freeze
53
+ IPATH_ABSOLUTE = Regexp.compile("/(?:(?:#{ISEGMENT_NZ})(/#{ISEGMENT})*)?").freeze
54
+ IPATH_NOSCHEME = Regexp.compile("(?:#{ISEGMENT_NZ_NC})(?:/#{ISEGMENT})*").freeze
55
+ IPATH_ROOTLESS = Regexp.compile("(?:#{ISEGMENT_NZ})(?:/#{ISEGMENT})*").freeze
56
+ IPATH_EMPTY = Regexp.compile("").freeze
57
+
58
+ IREG_NAME = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS}))*").freeze
59
+ IHOST = Regexp.compile("(?:#{IP_LITERAL})|(?:#{IREG_NAME})").freeze
60
+ IUSERINFO = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|:)*").freeze
61
+ IAUTHORITY = Regexp.compile("(?:#{IUSERINFO}@)?#{IHOST}(?::#{PORT})?").freeze
62
+
63
+ IRELATIVE_PART = Regexp.compile("(?:(?://#{IAUTHORITY}(?:#{IPATH_ABEMPTY}))|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_NOSCHEME})|(?:#{IPATH_EMPTY}))").freeze
64
+ IRELATIVE_REF = Regexp.compile("^#{IRELATIVE_PART}(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
65
+
66
+ IHIER_PART = Regexp.compile("(?:(?://#{IAUTHORITY}#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_ROOTLESS})|(?:#{IPATH_EMPTY}))").freeze
67
+ IRI = Regexp.compile("^#{SCHEME}:(?:#{IHIER_PART})(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
68
+
69
+ def valid_format?(data, format)
70
+ case format
71
+ when 'date-time'
72
+ valid_date_time?(data)
73
+ when 'date'
74
+ valid_date_time?("#{data}T04:05:06.123456789+07:00")
75
+ when 'time'
76
+ valid_date_time?("2001-02-03T#{data}")
77
+ when 'email'
78
+ data.ascii_only? && valid_email?(data)
79
+ when 'idn-email'
80
+ valid_email?(data)
81
+ when 'hostname'
82
+ data.ascii_only? && valid_hostname?(data)
83
+ when 'idn-hostname'
84
+ valid_hostname?(data)
85
+ when 'ipv4'
86
+ valid_ip?(data, :v4)
87
+ when 'ipv6'
88
+ valid_ip?(data, :v6)
89
+ when 'uri'
90
+ data.ascii_only? && valid_iri?(data)
91
+ when 'uri-reference'
92
+ data.ascii_only? && (valid_iri?(data) || valid_iri_reference?(data))
93
+ when 'iri'
94
+ valid_iri?(data)
95
+ when 'iri-reference'
96
+ valid_iri?(data) || valid_iri_reference?(data)
97
+ when 'uri-template'
98
+ valid_uri_template?(data)
99
+ when 'json-pointer'
100
+ valid_json_pointer?(data)
101
+ when 'relative-json-pointer'
102
+ valid_relative_json_pointer?(data)
103
+ when 'regex'
104
+ EcmaReValidator.valid?(data)
105
+ end
106
+ end
107
+
108
+ def valid_json?(data)
109
+ JSON.parse(data)
110
+ true
111
+ rescue JSON::ParserError
112
+ false
113
+ end
114
+
115
+ def valid_date_time?(data)
116
+ DateTime.rfc3339(data)
117
+ true
118
+ rescue ArgumentError => e
119
+ raise e unless e.message == 'invalid date'
120
+ false
121
+ end
122
+
123
+ def valid_email?(data)
124
+ !!(EMAIL_REGEX =~ data)
125
+ end
126
+
127
+ def valid_hostname?(data)
128
+ !!(HOSTNAME_REGEX =~ data && data.split('.').all? { |label| label.size <= 63 })
129
+ end
130
+
131
+ def valid_ip?(data, type)
132
+ ip_address = IPAddr.new(data)
133
+ type == :v4 ? ip_address.ipv4? : ip_address.ipv6?
134
+ rescue IPAddr::InvalidAddressError
135
+ false
136
+ end
137
+
138
+ def valid_iri?(data)
139
+ !!(IRI =~ data)
140
+ end
141
+
142
+ def valid_iri_reference?(data)
143
+ !!(IRELATIVE_REF =~ data)
144
+ end
145
+
146
+ def valid_uri_template?(data)
147
+ URITemplate.new(data)
148
+ true
149
+ rescue URITemplate::Invalid
150
+ false
151
+ end
152
+
153
+ def valid_json_pointer?(data)
154
+ !!(JSON_POINTER_REGEX =~ data)
155
+ end
156
+
157
+ def valid_relative_json_pointer?(data)
158
+ !!(RELATIVE_JSON_POINTER_REGEX =~ data)
159
+ end
160
+ end
61
161
  end
@@ -0,0 +1,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'hana'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'time'
8
+ require 'uri'
9
+
10
+ module JSONSchemer
11
+ module Schema
12
+ class Base
13
+ include Format
14
+
15
+ ID_KEYWORD = '$id'
16
+ DEFAULT_REF_RESOLVER = proc { |uri| raise UnknownRef, uri.to_s }.freeze
17
+ NET_HTTP_REF_RESOLVER = proc { |uri| JSON.parse(Net::HTTP.get(uri)) }.freeze
18
+ BOOLEANS = Set[true, false].freeze
19
+
20
+ def initialize(
21
+ schema,
22
+ format: true,
23
+ formats: nil,
24
+ keywords: nil,
25
+ ref_resolver: DEFAULT_REF_RESOLVER
26
+ )
27
+ @root = schema
28
+ @format = format
29
+ @formats = formats
30
+ @keywords = keywords
31
+ @ref_resolver = ref_resolver == 'net/http' ? NET_HTTP_REF_RESOLVER : ref_resolver
32
+ end
33
+
34
+ def valid?(data, schema = root, pointer = '', parent_uri = nil)
35
+ validate(data, schema, pointer, parent_uri).none?
36
+ end
37
+
38
+ def validate(data, schema = root, pointer = '', parent_uri = nil)
39
+ return enum_for(:validate, data, schema, pointer, parent_uri) unless block_given?
40
+
41
+ return if schema == true
42
+ if schema == false
43
+ yield error(data, schema, pointer, 'schema')
44
+ return
45
+ end
46
+
47
+ return if schema.empty?
48
+
49
+ type = schema['type']
50
+ enum = schema['enum']
51
+ all_of = schema['allOf']
52
+ any_of = schema['anyOf']
53
+ one_of = schema['oneOf']
54
+ not_schema = schema['not']
55
+ if_schema = schema['if']
56
+ then_schema = schema['then']
57
+ else_schema = schema['else']
58
+ format = schema['format']
59
+ ref = schema['$ref']
60
+ id = schema[id_keyword]
61
+
62
+ parent_uri = join_uri(parent_uri, id)
63
+
64
+ if ref
65
+ validate_ref(data, schema, pointer, parent_uri, ref, &Proc.new)
66
+ return
67
+ end
68
+
69
+ validate_format(data, schema, pointer, format, &Proc.new) if format && format?
70
+
71
+ if keywords
72
+ keywords.each do |keyword, callable|
73
+ if schema.key?(keyword)
74
+ result = callable.call(data, schema, pointer)
75
+ if result.is_a?(Array)
76
+ result.each { |error| yield error }
77
+ elsif !result
78
+ yield error(data, schema, pointer, keyword)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ yield error(data, schema, pointer, 'enum') if enum && !enum.include?(data)
85
+ yield error(data, schema, pointer, 'const') if schema.key?('const') && schema['const'] != data
86
+
87
+ yield error(data, schema, pointer, 'allOf') if all_of && !all_of.all? { |subschema| valid?(data, subschema, pointer, parent_uri) }
88
+ yield error(data, schema, pointer, 'anyOf') if any_of && !any_of.any? { |subschema| valid?(data, subschema, pointer, parent_uri) }
89
+ yield error(data, schema, pointer, 'oneOf') if one_of && !one_of.one? { |subschema| valid?(data, subschema, pointer, parent_uri) }
90
+ yield error(data, schema, pointer, 'not') if !not_schema.nil? && valid?(data, not_schema, pointer, parent_uri)
91
+
92
+ if if_schema && valid?(data, if_schema, pointer, parent_uri)
93
+ yield error(data, schema, pointer, 'then') if !then_schema.nil? && !valid?(data, then_schema, pointer, parent_uri)
94
+ elsif if_schema
95
+ yield error(data, schema, pointer, 'else') if !else_schema.nil? && !valid?(data, else_schema, pointer, parent_uri)
96
+ end
97
+
98
+ case type
99
+ when nil
100
+ validate_class(data, schema, pointer, parent_uri, &Proc.new)
101
+ when String
102
+ validate_type(data, schema, pointer, parent_uri, type, &Proc.new)
103
+ when Array
104
+ if valid_type = type.find { |subtype| valid?(data, { 'type' => subtype }, pointer, parent_uri) }
105
+ validate_type(data, schema, pointer, parent_uri, valid_type, &Proc.new)
106
+ else
107
+ yield error(data, schema, pointer, 'type')
108
+ end
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ def ids
115
+ @ids ||= resolve_ids(root)
116
+ end
117
+
118
+ private
119
+
120
+ attr_reader :root, :formats, :keywords, :ref_resolver
121
+
122
+ def id_keyword
123
+ ID_KEYWORD
124
+ end
125
+
126
+ def format?
127
+ !!@format
128
+ end
129
+
130
+ def child(schema)
131
+ JSONSchemer.schema(
132
+ schema,
133
+ format: format?,
134
+ formats: formats,
135
+ keywords: keywords,
136
+ ref_resolver: ref_resolver
137
+ )
138
+ end
139
+
140
+ def error(data, schema, pointer, type)
141
+ {
142
+ 'data' => data,
143
+ 'schema' => schema,
144
+ 'pointer' => pointer,
145
+ 'type' => type,
146
+ }
147
+ end
148
+
149
+ def validate_class(data, schema, pointer, parent_uri)
150
+ case data
151
+ when Integer
152
+ validate_integer(data, schema, pointer, &Proc.new)
153
+ when Numeric
154
+ validate_number(data, schema, pointer, &Proc.new)
155
+ when String
156
+ validate_string(data, schema, pointer, &Proc.new)
157
+ when Array
158
+ validate_array(data, schema, pointer, parent_uri, &Proc.new)
159
+ when Hash
160
+ validate_object(data, schema, pointer, parent_uri, &Proc.new)
161
+ end
162
+ end
163
+
164
+ def validate_type(data, schema, pointer, parent_uri, type)
165
+ case type
166
+ when 'null'
167
+ yield error(data, schema, pointer, 'null') unless data.nil?
168
+ when 'boolean'
169
+ yield error(data, schema, pointer, 'boolean') unless BOOLEANS.include?(data)
170
+ when 'number'
171
+ validate_number(data, schema, pointer, &Proc.new)
172
+ when 'integer'
173
+ validate_integer(data, schema, pointer, &Proc.new)
174
+ when 'string'
175
+ validate_string(data, schema, pointer, &Proc.new)
176
+ when 'array'
177
+ validate_array(data, schema, pointer, parent_uri, &Proc.new)
178
+ when 'object'
179
+ validate_object(data, schema, pointer, parent_uri, &Proc.new)
180
+ end
181
+ end
182
+
183
+ def validate_ref(data, schema, pointer, parent_uri, ref)
184
+ ref_uri = join_uri(parent_uri, ref)
185
+
186
+ if valid_json_pointer?(ref_uri.fragment)
187
+ ref_pointer = Hana::Pointer.new(URI.unescape(ref_uri.fragment || ''))
188
+ if ref.start_with?('#')
189
+ validate(data, ref_pointer.eval(root), pointer, pointer_uri(root, ref_pointer), &Proc.new)
190
+ else
191
+ ref_root = ref_resolver.call(ref_uri)
192
+ ref_object = child(ref_root)
193
+ ref_object.validate(data, ref_pointer.eval(ref_root), pointer, pointer_uri(ref_root, ref_pointer), &Proc.new)
194
+ end
195
+ elsif ids.key?(ref_uri.to_s)
196
+ validate(data, ids.fetch(ref_uri.to_s), pointer, ref_uri, &Proc.new)
197
+ else
198
+ ref_root = ref_resolver.call(ref_uri)
199
+ ref_object = child(ref_root)
200
+ ref_object.validate(data, ref_object.ids.fetch(ref_uri.to_s, ref_root), pointer, ref_uri, &Proc.new)
201
+ end
202
+ end
203
+
204
+ def validate_format(data, schema, pointer, format)
205
+ valid = if formats && formats.key?(format)
206
+ format_option = formats[format]
207
+ format_option == false || format_option.call(data, schema)
208
+ elsif supported_format?(format)
209
+ valid_format?(data, format)
210
+ end
211
+ yield error(data, schema, pointer, 'format') unless valid
212
+ end
213
+
214
+ def validate_exclusive_maximum(data, schema, pointer, exclusive_maximum, maximum)
215
+ yield error(data, schema, pointer, 'exclusiveMaximum') if data >= exclusive_maximum
216
+ end
217
+
218
+ def validate_exclusive_minimum(data, schema, pointer, exclusive_minimum, minimum)
219
+ yield error(data, schema, pointer, 'exclusiveMinimum') if data <= exclusive_minimum
220
+ end
221
+
222
+ def validate_numeric(data, schema, pointer)
223
+ multiple_of = schema['multipleOf']
224
+ maximum = schema['maximum']
225
+ exclusive_maximum = schema['exclusiveMaximum']
226
+ minimum = schema['minimum']
227
+ exclusive_minimum = schema['exclusiveMinimum']
228
+
229
+ yield error(data, schema, pointer, 'maximum') if maximum && data > maximum
230
+ yield error(data, schema, pointer, 'minimum') if minimum && data < minimum
231
+
232
+ validate_exclusive_maximum(data, schema, pointer, exclusive_maximum, maximum, &Proc.new) if exclusive_maximum
233
+ validate_exclusive_minimum(data, schema, pointer, exclusive_minimum, minimum, &Proc.new) if exclusive_minimum
234
+
235
+ if multiple_of
236
+ quotient = data / multiple_of.to_f
237
+ yield error(data, schema, pointer, 'multipleOf') unless quotient.floor == quotient
238
+ end
239
+ end
240
+
241
+ def validate_number(data, schema, pointer)
242
+ unless data.is_a?(Numeric)
243
+ yield error(data, schema, pointer, 'number')
244
+ return
245
+ end
246
+
247
+ validate_numeric(data, schema, pointer, &Proc.new)
248
+ end
249
+
250
+ def validate_integer(data, schema, pointer)
251
+ if !data.is_a?(Numeric) || (!data.is_a?(Integer) && data.floor != data)
252
+ yield error(data, schema, pointer, 'integer')
253
+ return
254
+ end
255
+
256
+ validate_numeric(data, schema, pointer, &Proc.new)
257
+ end
258
+
259
+ def validate_string(data, schema, pointer)
260
+ unless data.is_a?(String)
261
+ yield error(data, schema, pointer, 'string')
262
+ return
263
+ end
264
+
265
+ max_length = schema['maxLength']
266
+ min_length = schema['minLength']
267
+ pattern = schema['pattern']
268
+ content_encoding = schema['contentEncoding']
269
+ content_media_type = schema['contentMediaType']
270
+
271
+ yield error(data, schema, pointer, 'maxLength') if max_length && data.size > max_length
272
+ yield error(data, schema, pointer, 'minLength') if min_length && data.size < min_length
273
+ yield error(data, schema, pointer, 'pattern') if pattern && Regexp.new(pattern) !~ data
274
+
275
+ if content_encoding || content_media_type
276
+ decoded_data = data
277
+
278
+ if content_encoding
279
+ decoded_data = case content_encoding.downcase
280
+ when 'base64'
281
+ safe_strict_decode64(data)
282
+ else # '7bit', '8bit', 'binary', 'quoted-printable'
283
+ raise NotImplementedError
284
+ end
285
+ yield error(data, schema, pointer, 'contentEncoding') unless decoded_data
286
+ end
287
+
288
+ if content_media_type && decoded_data
289
+ case content_media_type.downcase
290
+ when 'application/json'
291
+ yield error(data, schema, pointer, 'contentMediaType') unless valid_json?(decoded_data)
292
+ else
293
+ raise NotImplementedError
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ def validate_array(data, schema, pointer, parent_uri, &block)
300
+ unless data.is_a?(Array)
301
+ yield error(data, schema, pointer, 'array')
302
+ return
303
+ end
304
+
305
+ items = schema['items']
306
+ additional_items = schema['additionalItems']
307
+ max_items = schema['maxItems']
308
+ min_items = schema['minItems']
309
+ unique_items = schema['uniqueItems']
310
+ contains = schema['contains']
311
+
312
+ yield error(data, schema, pointer, 'maxItems') if max_items && data.size > max_items
313
+ yield error(data, schema, pointer, 'minItems') if min_items && data.size < min_items
314
+ yield error(data, schema, pointer, 'uniqueItems') if unique_items && data.size != data.uniq.size
315
+ yield error(data, schema, pointer, 'contains') if !contains.nil? && data.all? { |item| !valid?(item, contains, pointer, parent_uri) }
316
+
317
+ if items.is_a?(Array)
318
+ data.each_with_index do |item, index|
319
+ if index < items.size
320
+ validate(item, items[index], "#{pointer}/#{index}", parent_uri, &block)
321
+ elsif !additional_items.nil?
322
+ validate(item, additional_items, "#{pointer}/#{index}", parent_uri, &block)
323
+ else
324
+ break
325
+ end
326
+ end
327
+ elsif !items.nil?
328
+ data.each_with_index do |item, index|
329
+ validate(item, items, "#{pointer}/#{index}", parent_uri, &block)
330
+ end
331
+ end
332
+ end
333
+
334
+ def validate_object(data, schema, pointer, parent_uri, &block)
335
+ unless data.is_a?(Hash)
336
+ yield error(data, schema, pointer, 'object')
337
+ return
338
+ end
339
+
340
+ max_properties = schema['maxProperties']
341
+ min_properties = schema['minProperties']
342
+ required = schema['required']
343
+ properties = schema['properties']
344
+ pattern_properties = schema['patternProperties']
345
+ additional_properties = schema['additionalProperties']
346
+ dependencies = schema['dependencies']
347
+ property_names = schema['propertyNames']
348
+
349
+ if dependencies
350
+ dependencies.each do |key, value|
351
+ next unless data.key?(key)
352
+ subschema = value.is_a?(Array) ? { 'required' => value } : value
353
+ validate(data, subschema, pointer, parent_uri, &block)
354
+ end
355
+ end
356
+
357
+ yield error(data, schema, pointer, 'maxProperties') if max_properties && data.size > max_properties
358
+ yield error(data, schema, pointer, 'minProperties') if min_properties && data.size < min_properties
359
+ yield error(data, schema, pointer, 'required') if required && required.any? { |key| !data.key?(key) }
360
+
361
+ regex_pattern_properties = nil
362
+ data.each do |key, value|
363
+ validate(key, property_names, pointer, parent_uri, &block) unless property_names.nil?
364
+
365
+ matched_key = false
366
+
367
+ if properties && properties.key?(key)
368
+ validate(value, properties[key], "#{pointer}/#{key}", parent_uri, &block)
369
+ matched_key = true
370
+ end
371
+
372
+ if pattern_properties
373
+ regex_pattern_properties ||= pattern_properties.map do |pattern, property_schema|
374
+ [Regexp.new(pattern), property_schema]
375
+ end
376
+ regex_pattern_properties.each do |regex, property_schema|
377
+ if regex =~ key
378
+ validate(value, property_schema, "#{pointer}/#{key}", parent_uri, &block)
379
+ matched_key = true
380
+ end
381
+ end
382
+ end
383
+
384
+ next if matched_key
385
+
386
+ validate(value, additional_properties, "#{pointer}/#{key}", parent_uri, &block) unless additional_properties.nil?
387
+ end
388
+ end
389
+
390
+ def safe_strict_decode64(data)
391
+ begin
392
+ Base64.strict_decode64(data)
393
+ rescue ArgumentError => e
394
+ raise e unless e.message == 'invalid base64'
395
+ nil
396
+ end
397
+ end
398
+
399
+ def join_uri(a, b)
400
+ if a && b
401
+ URI.join(a, b)
402
+ elsif b
403
+ URI.parse(b)
404
+ else
405
+ a
406
+ end
407
+ end
408
+
409
+ def pointer_uri(schema, pointer)
410
+ uri_parts = nil
411
+ pointer.reduce(schema) do |obj, token|
412
+ next obj.fetch(token.to_i) if obj.is_a?(Array)
413
+ if obj_id = obj[id_keyword]
414
+ uri_parts ||= []
415
+ uri_parts << obj_id
416
+ end
417
+ obj.fetch(token)
418
+ end
419
+ uri_parts ? URI.join(*uri_parts) : nil
420
+ end
421
+
422
+ def resolve_ids(schema, ids = {}, parent_uri = nil)
423
+ if schema.is_a?(Array)
424
+ schema.each { |subschema| resolve_ids(subschema, ids, parent_uri) }
425
+ elsif schema.is_a?(Hash)
426
+ id = schema[id_keyword]
427
+ uri = join_uri(parent_uri, id)
428
+ ids[uri.to_s] = schema unless uri == parent_uri
429
+ if definitions = schema['definitions']
430
+ definitions.each_value { |subschema| resolve_ids(subschema, ids, uri) }
431
+ end
432
+ end
433
+ ids
434
+ end
435
+ end
436
+ end
437
+ end