json_schemer 0.1.3 → 0.1.4

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,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