senko 0.1.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.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/Rakefile +56 -0
- data/ext/senko/extconf.rb +5 -0
- data/ext/senko/instructions.c +1 -0
- data/ext/senko/instructions.h +12 -0
- data/ext/senko/senko.c +17 -0
- data/ext/senko/validator.c +107 -0
- data/ext/senko/validator.h +14 -0
- data/lib/senko/cache.rb +57 -0
- data/lib/senko/code_generator.rb +270 -0
- data/lib/senko/compiler/instruction.rb +69 -0
- data/lib/senko/compiler/optimizer.rb +80 -0
- data/lib/senko/compiler/ref_resolver.rb +409 -0
- data/lib/senko/compiler.rb +991 -0
- data/lib/senko/dialect.rb +59 -0
- data/lib/senko/errors.rb +41 -0
- data/lib/senko/format.rb +327 -0
- data/lib/senko/native.rb +11 -0
- data/lib/senko/result.rb +65 -0
- data/lib/senko/schema.rb +58 -0
- data/lib/senko/validator.rb +1391 -0
- data/lib/senko/version.rb +5 -0
- data/lib/senko/vocabulary.rb +25 -0
- data/lib/senko.rb +171 -0
- data/sig/senko.rbs +45 -0
- metadata +170 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
require_relative 'compiler/instruction'
|
|
7
|
+
require_relative 'compiler/optimizer'
|
|
8
|
+
require_relative 'compiler/ref_resolver'
|
|
9
|
+
require_relative 'dialect'
|
|
10
|
+
require_relative 'errors'
|
|
11
|
+
|
|
12
|
+
module Senko
|
|
13
|
+
class Compiler
|
|
14
|
+
DEFAULT_OPTIONS = {
|
|
15
|
+
draft: nil,
|
|
16
|
+
format: :annotation,
|
|
17
|
+
ref_resolver: nil,
|
|
18
|
+
schemas: {},
|
|
19
|
+
fail_fast: false,
|
|
20
|
+
validate_meta_schema: false,
|
|
21
|
+
custom_formats: {},
|
|
22
|
+
custom_keywords: {},
|
|
23
|
+
codegen: :auto,
|
|
24
|
+
messages: {}
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
APPLICATOR_KEYWORDS = %w[allOf anyOf oneOf].freeze
|
|
28
|
+
SCHEMA_ARRAY_KEYWORDS = %w[allOf anyOf oneOf prefixItems].freeze
|
|
29
|
+
SCHEMA_MAP_KEYWORDS = %w[properties patternProperties $defs definitions dependentSchemas].freeze
|
|
30
|
+
SCHEMA_VALUE_KEYWORDS = %w[
|
|
31
|
+
items additionalItems additionalProperties propertyNames contains not if then else
|
|
32
|
+
unevaluatedProperties unevaluatedItems
|
|
33
|
+
].freeze
|
|
34
|
+
DATA_VALUE_KEYWORDS = %w[const enum default examples required dependentRequired].freeze
|
|
35
|
+
|
|
36
|
+
attr_reader :options
|
|
37
|
+
|
|
38
|
+
def initialize(options = {})
|
|
39
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
40
|
+
@ref_cache = {}
|
|
41
|
+
@resolving = Set.new
|
|
42
|
+
@instruction_arrays = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def compile(schema)
|
|
46
|
+
stringified = deep_stringify(schema)
|
|
47
|
+
validate_meta_schema!(stringified) if @options[:validate_meta_schema]
|
|
48
|
+
@draft = Dialect.detect(stringified, @options[:draft])
|
|
49
|
+
@root_schema = normalize_schema(stringified, @draft)
|
|
50
|
+
@ref_resolver = RefResolver.new(
|
|
51
|
+
@root_schema,
|
|
52
|
+
schemas: @options[:schemas] || {},
|
|
53
|
+
ref_resolver: @options[:ref_resolver]
|
|
54
|
+
)
|
|
55
|
+
@root_scope = RefResolver::ROOT_SCOPE
|
|
56
|
+
@validation_vocabulary_enabled = validation_vocabulary_enabled?(@root_schema)
|
|
57
|
+
|
|
58
|
+
Optimizer.new.optimize(compile_schema(@root_schema, '', @root_scope))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def compile_schema(schema, path, scope)
|
|
64
|
+
case schema
|
|
65
|
+
when true
|
|
66
|
+
[]
|
|
67
|
+
when false
|
|
68
|
+
[instruction(:false_schema, {}, path, schema)]
|
|
69
|
+
when Hash
|
|
70
|
+
compile_hash_schema(schema, path, scope)
|
|
71
|
+
else
|
|
72
|
+
raise CompileError, 'schema must be a Hash, true, or false'
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def compile_hash_schema(schema, path, scope)
|
|
77
|
+
instructions = []
|
|
78
|
+
@instruction_arrays[[scope, path]] = instructions
|
|
79
|
+
instructions.concat(compile_ref(schema, path, scope))
|
|
80
|
+
instructions.concat(compile_dynamic_ref(schema, path, scope))
|
|
81
|
+
if @validation_vocabulary_enabled
|
|
82
|
+
compile_type(schema, path, instructions)
|
|
83
|
+
compile_enum(schema, path, instructions)
|
|
84
|
+
compile_const(schema, path, instructions)
|
|
85
|
+
compile_numeric(schema, path, instructions)
|
|
86
|
+
compile_string(schema, path, instructions)
|
|
87
|
+
end
|
|
88
|
+
compile_format(schema, path, instructions)
|
|
89
|
+
compile_array(schema, path, scope, instructions)
|
|
90
|
+
compile_object(schema, path, scope, instructions)
|
|
91
|
+
compile_applicators(schema, path, scope, instructions)
|
|
92
|
+
compile_unevaluated(schema, path, scope, instructions)
|
|
93
|
+
compile_custom_keywords(schema, path, instructions)
|
|
94
|
+
compile_dynamic_scope(schema, path, scope, instructions)
|
|
95
|
+
instructions
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def compile_ref(schema, path, scope)
|
|
99
|
+
return [] unless schema.key?('$ref')
|
|
100
|
+
|
|
101
|
+
uri = schema.fetch('$ref').to_s
|
|
102
|
+
resolved = @ref_resolver.resolve(uri, from: path, scope: scope)
|
|
103
|
+
target_instructions = compile_ref_target(ref_cache_key(uri, resolved), resolved)
|
|
104
|
+
|
|
105
|
+
[
|
|
106
|
+
instruction(
|
|
107
|
+
:ref,
|
|
108
|
+
{ uri: uri, instructions: target_instructions },
|
|
109
|
+
join_pointer(path, '$ref'),
|
|
110
|
+
schema['$ref']
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def compile_dynamic_ref(schema, path, scope)
|
|
116
|
+
return [] unless schema.key?('$dynamicRef')
|
|
117
|
+
|
|
118
|
+
uri = schema.fetch('$dynamicRef').to_s
|
|
119
|
+
resolved = @ref_resolver.resolve_dynamic(uri, from: path, scope: scope)
|
|
120
|
+
target_instructions = compile_ref_target(ref_cache_key(uri, resolved, prefix: 'dynamic'), resolved)
|
|
121
|
+
anchor = dynamic_ref_anchor(uri)
|
|
122
|
+
dynamic = anchor && resolved.schema.is_a?(Hash) && resolved.schema['$dynamicAnchor'] == anchor
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
instruction(
|
|
126
|
+
:dynamic_ref,
|
|
127
|
+
{ uri: uri, anchor: anchor, dynamic: dynamic, instructions: target_instructions },
|
|
128
|
+
join_pointer(path, '$dynamicRef'),
|
|
129
|
+
schema['$dynamicRef']
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def compile_ref_target(uri, resolved)
|
|
135
|
+
return @ref_cache[uri] if @ref_cache.key?(uri)
|
|
136
|
+
|
|
137
|
+
placeholder = []
|
|
138
|
+
@ref_cache[uri] = placeholder
|
|
139
|
+
@resolving.add(uri)
|
|
140
|
+
normalized = normalize_schema(deep_stringify(resolved.schema), @draft)
|
|
141
|
+
target_draft = Dialect.schema_draft(normalized) || @draft
|
|
142
|
+
previous_draft = @draft
|
|
143
|
+
@draft = target_draft
|
|
144
|
+
normalized = normalize_schema(normalized, target_draft)
|
|
145
|
+
placeholder.replace(compile_schema(normalized, resolved.keyword_location, resolved.scope))
|
|
146
|
+
placeholder
|
|
147
|
+
rescue StandardError
|
|
148
|
+
@ref_cache.delete(uri) if @ref_cache[uri].equal?(placeholder)
|
|
149
|
+
raise
|
|
150
|
+
ensure
|
|
151
|
+
@draft = previous_draft if defined?(previous_draft) && previous_draft
|
|
152
|
+
@resolving.delete(uri)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def ref_cache_key(uri, resolved, prefix: 'ref')
|
|
156
|
+
"#{prefix}:#{resolved.scope}:#{resolved.base_uri}##{resolved.keyword_location}:#{uri}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def compile_dynamic_scope(_schema, path, scope, instructions)
|
|
160
|
+
@instruction_arrays[[scope, path]] = instructions
|
|
161
|
+
base_uri = @ref_resolver.base_uri_for(path, scope)
|
|
162
|
+
anchors = @ref_resolver.dynamic_anchors_for(base_uri)
|
|
163
|
+
return if anchors.empty?
|
|
164
|
+
|
|
165
|
+
compiled = anchors.each_with_object({}) do |(anchor, resolved), result|
|
|
166
|
+
result[anchor] = @instruction_arrays[[resolved.scope, resolved.keyword_location]] ||
|
|
167
|
+
compile_ref_target(ref_cache_key(anchor, resolved, prefix: 'dynamic-anchor'), resolved)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
instructions.unshift(
|
|
171
|
+
instruction(
|
|
172
|
+
:dynamic_scope,
|
|
173
|
+
{ base_uri: base_uri, anchors: compiled },
|
|
174
|
+
path,
|
|
175
|
+
nil
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def dynamic_ref_anchor(uri)
|
|
181
|
+
_base, fragment = uri.to_s.split('#', 2)
|
|
182
|
+
return nil if fragment.nil? || fragment.empty? || fragment.start_with?('/')
|
|
183
|
+
|
|
184
|
+
URI::DEFAULT_PARSER.unescape(fragment)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def compile_type(schema, path, instructions)
|
|
188
|
+
return unless schema.key?('type')
|
|
189
|
+
|
|
190
|
+
type_value = schema.fetch('type')
|
|
191
|
+
types = type_value.is_a?(Array) ? type_value : [type_value]
|
|
192
|
+
mask = types.reduce(0) do |memo, type|
|
|
193
|
+
type_name = type.to_s
|
|
194
|
+
bit = Instructions::TYPE_MAP[type_name]
|
|
195
|
+
raise CompileError, "unsupported JSON Schema type: #{type.inspect}" unless bit
|
|
196
|
+
|
|
197
|
+
memo | bit
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
instructions << instruction(
|
|
201
|
+
:type,
|
|
202
|
+
{ mask: mask, expected: types.map(&:to_s) },
|
|
203
|
+
join_pointer(path, 'type'),
|
|
204
|
+
type_value
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def compile_enum(schema, path, instructions)
|
|
209
|
+
return unless schema.key?('enum')
|
|
210
|
+
|
|
211
|
+
values = schema.fetch('enum')
|
|
212
|
+
raise CompileError, 'enum must be an Array' unless values.is_a?(Array)
|
|
213
|
+
|
|
214
|
+
instructions << instruction(:enum, { values: values }, join_pointer(path, 'enum'), values)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def compile_const(schema, path, instructions)
|
|
218
|
+
return unless schema.key?('const')
|
|
219
|
+
|
|
220
|
+
instructions << instruction(:const, { value: schema.fetch('const') }, join_pointer(path, 'const'),
|
|
221
|
+
schema['const'])
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def compile_numeric(schema, path, instructions)
|
|
225
|
+
if schema.key?('multipleOf')
|
|
226
|
+
factor = numeric_value!(schema.fetch('multipleOf'), 'multipleOf')
|
|
227
|
+
raise CompileError, 'multipleOf must be greater than 0' unless factor.positive?
|
|
228
|
+
|
|
229
|
+
instructions << instruction(:multiple_of, { factor: factor }, join_pointer(path, 'multipleOf'), factor)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if schema.key?('maximum')
|
|
233
|
+
limit = numeric_value!(schema.fetch('maximum'), 'maximum')
|
|
234
|
+
instructions << instruction(:maximum, { limit: limit, exclusive: false }, join_pointer(path, 'maximum'), limit)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if schema.key?('exclusiveMaximum')
|
|
238
|
+
limit = numeric_value!(schema.fetch('exclusiveMaximum'), 'exclusiveMaximum')
|
|
239
|
+
instructions << instruction(:maximum, { limit: limit, exclusive: true }, join_pointer(path, 'exclusiveMaximum'),
|
|
240
|
+
limit)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
if schema.key?('minimum')
|
|
244
|
+
limit = numeric_value!(schema.fetch('minimum'), 'minimum')
|
|
245
|
+
instructions << instruction(:minimum, { limit: limit, exclusive: false }, join_pointer(path, 'minimum'), limit)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
return unless schema.key?('exclusiveMinimum')
|
|
249
|
+
|
|
250
|
+
limit = numeric_value!(schema.fetch('exclusiveMinimum'), 'exclusiveMinimum')
|
|
251
|
+
instructions << instruction(:minimum, { limit: limit, exclusive: true }, join_pointer(path, 'exclusiveMinimum'),
|
|
252
|
+
limit)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def compile_string(schema, path, instructions)
|
|
256
|
+
if schema.key?('maxLength')
|
|
257
|
+
limit = non_negative_integer!(schema.fetch('maxLength'), 'maxLength')
|
|
258
|
+
instructions << instruction(:max_length, { limit: limit }, join_pointer(path, 'maxLength'), limit)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
if schema.key?('minLength')
|
|
262
|
+
limit = non_negative_integer!(schema.fetch('minLength'), 'minLength')
|
|
263
|
+
instructions << instruction(:min_length, { limit: limit }, join_pointer(path, 'minLength'), limit)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
return unless schema.key?('pattern')
|
|
267
|
+
|
|
268
|
+
pattern = schema.fetch('pattern').to_s
|
|
269
|
+
regexp = Regexp.new(Senko::Format.ecma_pattern_source(pattern))
|
|
270
|
+
instructions << instruction(:pattern, { pattern: regexp, source: pattern }, join_pointer(path, 'pattern'),
|
|
271
|
+
pattern)
|
|
272
|
+
rescue RegexpError => e
|
|
273
|
+
raise CompileError, "invalid pattern #{pattern.inspect}: #{e.message}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def compile_format(schema, path, instructions)
|
|
277
|
+
return unless schema.key?('format')
|
|
278
|
+
|
|
279
|
+
format = schema.fetch('format').to_s
|
|
280
|
+
instructions << instruction(
|
|
281
|
+
:format,
|
|
282
|
+
{ format: format, assertion: format_assertion_enabled? },
|
|
283
|
+
join_pointer(path, 'format'),
|
|
284
|
+
format
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def compile_array(schema, path, scope, instructions)
|
|
289
|
+
if @validation_vocabulary_enabled
|
|
290
|
+
if schema.key?('maxItems')
|
|
291
|
+
limit = non_negative_integer!(schema.fetch('maxItems'), 'maxItems')
|
|
292
|
+
instructions << instruction(:max_items, { limit: limit }, join_pointer(path, 'maxItems'), limit)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if schema.key?('minItems')
|
|
296
|
+
limit = non_negative_integer!(schema.fetch('minItems'), 'minItems')
|
|
297
|
+
instructions << instruction(:min_items, { limit: limit }, join_pointer(path, 'minItems'), limit)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
if schema['uniqueItems'] == true
|
|
301
|
+
instructions << instruction(:unique_items, {}, join_pointer(path, 'uniqueItems'),
|
|
302
|
+
true)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
prefix_count = compile_prefix_items(schema, path, scope, instructions)
|
|
307
|
+
compile_items(schema, path, scope, prefix_count, instructions)
|
|
308
|
+
compile_contains(schema, path, scope, instructions)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def compile_prefix_items(schema, path, scope, instructions)
|
|
312
|
+
return 0 unless schema.key?('prefixItems')
|
|
313
|
+
|
|
314
|
+
prefix_items = schema.fetch('prefixItems')
|
|
315
|
+
raise CompileError, 'prefixItems must be an Array' unless prefix_items.is_a?(Array)
|
|
316
|
+
|
|
317
|
+
compiled = prefix_items.each_with_index.map do |subschema, index|
|
|
318
|
+
compile_schema(subschema, join_pointer(path, 'prefixItems', index.to_s), scope)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
instructions << instruction(:prefix_items, { schemas: compiled }, join_pointer(path, 'prefixItems'), prefix_items)
|
|
322
|
+
compiled.length
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def compile_items(schema, path, scope, prefix_count, instructions)
|
|
326
|
+
return unless schema.key?('items')
|
|
327
|
+
|
|
328
|
+
instructions << instruction(
|
|
329
|
+
:items,
|
|
330
|
+
{ schema: compile_schema(schema.fetch('items'), join_pointer(path, 'items'), scope),
|
|
331
|
+
start_index: prefix_count },
|
|
332
|
+
join_pointer(path, 'items'),
|
|
333
|
+
schema['items']
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def compile_contains(schema, path, scope, instructions)
|
|
338
|
+
return unless schema.key?('contains')
|
|
339
|
+
|
|
340
|
+
min = if @validation_vocabulary_enabled && schema.key?('minContains')
|
|
341
|
+
non_negative_integer!(schema.fetch('minContains'), 'minContains')
|
|
342
|
+
else
|
|
343
|
+
1
|
|
344
|
+
end
|
|
345
|
+
max = if @validation_vocabulary_enabled && schema.key?('maxContains')
|
|
346
|
+
non_negative_integer!(schema.fetch('maxContains'), 'maxContains')
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
instructions << instruction(
|
|
350
|
+
:contains,
|
|
351
|
+
{
|
|
352
|
+
schema: compile_schema(schema.fetch('contains'), join_pointer(path, 'contains'), scope),
|
|
353
|
+
min: min,
|
|
354
|
+
max: max
|
|
355
|
+
},
|
|
356
|
+
join_pointer(path, 'contains'),
|
|
357
|
+
schema['contains']
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def compile_object(schema, path, scope, instructions)
|
|
362
|
+
if @validation_vocabulary_enabled
|
|
363
|
+
if schema.key?('maxProperties')
|
|
364
|
+
limit = non_negative_integer!(schema.fetch('maxProperties'), 'maxProperties')
|
|
365
|
+
instructions << instruction(:max_properties, { limit: limit }, join_pointer(path, 'maxProperties'), limit)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
if schema.key?('minProperties')
|
|
369
|
+
limit = non_negative_integer!(schema.fetch('minProperties'), 'minProperties')
|
|
370
|
+
instructions << instruction(:min_properties, { limit: limit }, join_pointer(path, 'minProperties'), limit)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
compile_required(schema, path, instructions)
|
|
374
|
+
compile_dependent_required(schema, path, instructions)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
compile_properties(schema, path, scope, instructions)
|
|
378
|
+
compile_pattern_properties(schema, path, scope, instructions)
|
|
379
|
+
compile_additional_properties(schema, path, scope, instructions)
|
|
380
|
+
compile_property_names(schema, path, scope, instructions)
|
|
381
|
+
compile_dependent_schemas(schema, path, scope, instructions)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def compile_required(schema, path, instructions)
|
|
385
|
+
return unless schema.key?('required')
|
|
386
|
+
|
|
387
|
+
required = schema.fetch('required')
|
|
388
|
+
raise CompileError, 'required must be an Array' unless required.is_a?(Array)
|
|
389
|
+
|
|
390
|
+
instructions << instruction(
|
|
391
|
+
:required,
|
|
392
|
+
{ keys: required.to_set(&:to_s) },
|
|
393
|
+
join_pointer(path, 'required'),
|
|
394
|
+
required
|
|
395
|
+
)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def compile_properties(schema, path, scope, instructions)
|
|
399
|
+
return unless schema.key?('properties')
|
|
400
|
+
|
|
401
|
+
properties = schema.fetch('properties')
|
|
402
|
+
raise CompileError, 'properties must be a Hash' unless properties.is_a?(Hash)
|
|
403
|
+
|
|
404
|
+
compiled = properties.each_with_object({}) do |(name, subschema), result|
|
|
405
|
+
key = name.to_s
|
|
406
|
+
result[key] = compile_schema(subschema, join_pointer(path, 'properties', key), scope)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
instructions << instruction(:properties, { schemas: compiled }, join_pointer(path, 'properties'), properties)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def compile_pattern_properties(schema, path, scope, instructions)
|
|
413
|
+
return unless schema.key?('patternProperties')
|
|
414
|
+
|
|
415
|
+
pattern_properties = schema.fetch('patternProperties')
|
|
416
|
+
raise CompileError, 'patternProperties must be a Hash' unless pattern_properties.is_a?(Hash)
|
|
417
|
+
|
|
418
|
+
compiled = pattern_properties.map do |pattern, subschema|
|
|
419
|
+
regexp = Regexp.new(Senko::Format.ecma_pattern_source(pattern.to_s))
|
|
420
|
+
{
|
|
421
|
+
pattern: regexp,
|
|
422
|
+
source: pattern.to_s,
|
|
423
|
+
schema: compile_schema(subschema, join_pointer(path, 'patternProperties', pattern.to_s), scope)
|
|
424
|
+
}
|
|
425
|
+
rescue RegexpError => e
|
|
426
|
+
raise CompileError, "invalid patternProperties pattern #{pattern.inspect}: #{e.message}"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
instructions << instruction(:pattern_properties, { patterns: compiled }, join_pointer(path, 'patternProperties'),
|
|
430
|
+
pattern_properties)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def compile_additional_properties(schema, path, scope, instructions)
|
|
434
|
+
return unless schema.key?('additionalProperties')
|
|
435
|
+
|
|
436
|
+
additional = schema.fetch('additionalProperties')
|
|
437
|
+
|
|
438
|
+
patterns = (schema['patternProperties'] || {}).keys.map { |pattern| Regexp.new(pattern.to_s) }
|
|
439
|
+
payload = {
|
|
440
|
+
known: (schema['properties'] || {}).keys.to_set(&:to_s),
|
|
441
|
+
patterns: patterns,
|
|
442
|
+
schema: (if additional == false
|
|
443
|
+
false
|
|
444
|
+
else
|
|
445
|
+
compile_schema(additional, join_pointer(path, 'additionalProperties'),
|
|
446
|
+
scope)
|
|
447
|
+
end)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
instructions << instruction(:additional_properties, payload, join_pointer(path, 'additionalProperties'),
|
|
451
|
+
additional)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def compile_property_names(schema, path, scope, instructions)
|
|
455
|
+
return unless schema.key?('propertyNames')
|
|
456
|
+
|
|
457
|
+
instructions << instruction(
|
|
458
|
+
:property_names,
|
|
459
|
+
{ schema: compile_schema(schema.fetch('propertyNames'), join_pointer(path, 'propertyNames'), scope) },
|
|
460
|
+
join_pointer(path, 'propertyNames'),
|
|
461
|
+
schema['propertyNames']
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def compile_dependent_required(schema, path, instructions)
|
|
466
|
+
return unless schema.key?('dependentRequired')
|
|
467
|
+
|
|
468
|
+
requirements = schema.fetch('dependentRequired')
|
|
469
|
+
raise CompileError, 'dependentRequired must be a Hash' unless requirements.is_a?(Hash)
|
|
470
|
+
|
|
471
|
+
compiled = requirements.transform_values do |keys|
|
|
472
|
+
raise CompileError, 'dependentRequired values must be Arrays' unless keys.is_a?(Array)
|
|
473
|
+
|
|
474
|
+
keys.to_set(&:to_s)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
instructions << instruction(:dependent_required, { requirements: compiled },
|
|
478
|
+
join_pointer(path, 'dependentRequired'), requirements)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def compile_dependent_schemas(schema, path, scope, instructions)
|
|
482
|
+
return unless schema.key?('dependentSchemas')
|
|
483
|
+
|
|
484
|
+
dependent_schemas = schema.fetch('dependentSchemas')
|
|
485
|
+
raise CompileError, 'dependentSchemas must be a Hash' unless dependent_schemas.is_a?(Hash)
|
|
486
|
+
|
|
487
|
+
compiled = dependent_schemas.each_with_object({}) do |(key, subschema), result|
|
|
488
|
+
name = key.to_s
|
|
489
|
+
result[name] = compile_schema(subschema, join_pointer(path, 'dependentSchemas', name), scope)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
instructions << instruction(:dependent_schemas, { schemas: compiled }, join_pointer(path, 'dependentSchemas'),
|
|
493
|
+
dependent_schemas)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def compile_applicators(schema, path, scope, instructions)
|
|
497
|
+
APPLICATOR_KEYWORDS.each do |keyword|
|
|
498
|
+
next unless schema.key?(keyword)
|
|
499
|
+
|
|
500
|
+
subschemas = schema.fetch(keyword)
|
|
501
|
+
raise CompileError, "#{keyword} must be an Array" unless subschemas.is_a?(Array)
|
|
502
|
+
|
|
503
|
+
discriminator = detect_explicit_discriminator(schema, keyword, path, scope) || detect_discriminator(subschemas)
|
|
504
|
+
if discriminator && %w[anyOf oneOf].include?(keyword)
|
|
505
|
+
instructions << compile_discriminator(keyword, discriminator, path, scope, subschemas)
|
|
506
|
+
next
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
compiled = subschemas.each_with_index.map do |subschema, index|
|
|
510
|
+
compile_schema(subschema, join_pointer(path, keyword, index.to_s), scope)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
instructions << instruction(keyword_to_op(keyword), { schemas: compiled }, join_pointer(path, keyword),
|
|
514
|
+
subschemas)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
compile_not(schema, path, scope, instructions)
|
|
518
|
+
compile_if_then_else(schema, path, scope, instructions)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def compile_not(schema, path, scope, instructions)
|
|
522
|
+
return unless schema.key?('not')
|
|
523
|
+
|
|
524
|
+
instructions << instruction(
|
|
525
|
+
:not,
|
|
526
|
+
{ schema: compile_schema(schema.fetch('not'), join_pointer(path, 'not'), scope) },
|
|
527
|
+
join_pointer(path, 'not'),
|
|
528
|
+
schema['not']
|
|
529
|
+
)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def detect_discriminator(subschemas)
|
|
533
|
+
candidates = discriminator_candidates(subschemas.first)
|
|
534
|
+
candidates.each do |property|
|
|
535
|
+
mapping = discriminator_mapping(subschemas, property)
|
|
536
|
+
return { property: property, mapping: mapping } if mapping && mapping.length == subschemas.length
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
nil
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def detect_explicit_discriminator(schema, keyword, path, scope)
|
|
543
|
+
config = schema['discriminator']
|
|
544
|
+
return nil unless config.is_a?(Hash) && config['propertyName'].is_a?(String)
|
|
545
|
+
|
|
546
|
+
subschemas = schema[keyword]
|
|
547
|
+
property = config.fetch('propertyName')
|
|
548
|
+
mapping = explicit_discriminator_mapping(config.fetch('mapping', {}), subschemas, property, path, scope)
|
|
549
|
+
return nil unless mapping && !mapping.empty?
|
|
550
|
+
|
|
551
|
+
{ property: property, mapping: mapping }
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def explicit_discriminator_mapping(mapping_config, subschemas, property, path, scope)
|
|
555
|
+
if mapping_config.is_a?(Hash) && !mapping_config.empty?
|
|
556
|
+
return mapping_config.each_with_object({}) do |(value, ref), result|
|
|
557
|
+
resolved = @ref_resolver.resolve(ref.to_s, from: path, scope: scope)
|
|
558
|
+
result[value.to_s] = normalize_schema(deep_stringify(resolved.schema), @draft)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
discriminator_mapping(subschemas, property)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def discriminator_candidates(schema)
|
|
566
|
+
return [] unless schema.is_a?(Hash)
|
|
567
|
+
|
|
568
|
+
required = Array(schema['required']).map(&:to_s)
|
|
569
|
+
properties = schema['properties']
|
|
570
|
+
return [] unless properties.is_a?(Hash)
|
|
571
|
+
|
|
572
|
+
properties.each_with_object([]) do |(property, subschema), result|
|
|
573
|
+
key = property.to_s
|
|
574
|
+
result << key if required.include?(key) && subschema.is_a?(Hash) && subschema.key?('const')
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def discriminator_mapping(subschemas, property)
|
|
579
|
+
values = {}
|
|
580
|
+
|
|
581
|
+
subschemas.each do |subschema|
|
|
582
|
+
return nil unless subschema.is_a?(Hash)
|
|
583
|
+
return nil unless Array(subschema['required']).map(&:to_s).include?(property)
|
|
584
|
+
|
|
585
|
+
property_schema = subschema.fetch('properties', {})[property]
|
|
586
|
+
return nil unless property_schema.is_a?(Hash) && property_schema.key?('const')
|
|
587
|
+
|
|
588
|
+
value = property_schema.fetch('const')
|
|
589
|
+
return nil if values.key?(value)
|
|
590
|
+
|
|
591
|
+
values[value] = subschema
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
values
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def compile_discriminator(keyword, discriminator, path, scope, subschemas)
|
|
598
|
+
mapping = discriminator[:mapping].transform_values.with_index do |subschema, index|
|
|
599
|
+
compile_schema(subschema, join_pointer(path, keyword, index.to_s), scope)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
instruction(
|
|
603
|
+
:discriminator,
|
|
604
|
+
{
|
|
605
|
+
mode: keyword_to_op(keyword),
|
|
606
|
+
property: discriminator[:property],
|
|
607
|
+
mapping: mapping
|
|
608
|
+
},
|
|
609
|
+
join_pointer(path, keyword),
|
|
610
|
+
subschemas
|
|
611
|
+
)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def compile_if_then_else(schema, path, scope, instructions)
|
|
615
|
+
return unless schema.key?('if')
|
|
616
|
+
|
|
617
|
+
payload = {
|
|
618
|
+
if_schema: compile_schema(schema.fetch('if'), join_pointer(path, 'if'), scope),
|
|
619
|
+
then_schema: (compile_schema(schema.fetch('then'), join_pointer(path, 'then'), scope) if schema.key?('then')),
|
|
620
|
+
else_schema: (compile_schema(schema.fetch('else'), join_pointer(path, 'else'), scope) if schema.key?('else'))
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
instructions << instruction(:if_then_else, payload, join_pointer(path, 'if'), schema['if'])
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def compile_unevaluated(schema, path, scope, instructions)
|
|
627
|
+
if schema.key?('unevaluatedProperties')
|
|
628
|
+
value = schema.fetch('unevaluatedProperties')
|
|
629
|
+
instructions << instruction(
|
|
630
|
+
:unevaluated_properties,
|
|
631
|
+
{ schema: if value == false
|
|
632
|
+
false
|
|
633
|
+
else
|
|
634
|
+
compile_schema(value, join_pointer(path, 'unevaluatedProperties'),
|
|
635
|
+
scope)
|
|
636
|
+
end },
|
|
637
|
+
join_pointer(path, 'unevaluatedProperties'),
|
|
638
|
+
value
|
|
639
|
+
)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
return unless schema.key?('unevaluatedItems')
|
|
643
|
+
|
|
644
|
+
value = schema.fetch('unevaluatedItems')
|
|
645
|
+
instructions << instruction(
|
|
646
|
+
:unevaluated_items,
|
|
647
|
+
{ schema: value == false ? false : compile_schema(value, join_pointer(path, 'unevaluatedItems'), scope) },
|
|
648
|
+
join_pointer(path, 'unevaluatedItems'),
|
|
649
|
+
value
|
|
650
|
+
)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def compile_custom_keywords(schema, path, instructions)
|
|
654
|
+
(@options[:custom_keywords] || {}).each do |keyword, validator|
|
|
655
|
+
key = keyword.to_s
|
|
656
|
+
next unless schema.key?(key)
|
|
657
|
+
|
|
658
|
+
instructions << instruction(
|
|
659
|
+
:custom_keyword,
|
|
660
|
+
{ keyword: key, value: schema.fetch(key), validator: validator },
|
|
661
|
+
join_pointer(path, key),
|
|
662
|
+
schema.fetch(key)
|
|
663
|
+
)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def normalize_schema(value, draft, schema_position: true)
|
|
668
|
+
case value
|
|
669
|
+
when Hash
|
|
670
|
+
normalized = normalize_openapi_nullable(normalize_schema_hash(value, draft))
|
|
671
|
+
schema_position ? normalize_legacy_schema(normalized, draft) : normalized
|
|
672
|
+
when Array
|
|
673
|
+
value.map { |child| normalize_schema(child, draft, schema_position: false) }
|
|
674
|
+
else
|
|
675
|
+
value
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def validate_meta_schema!(schema, path = '')
|
|
680
|
+
case schema
|
|
681
|
+
when true, false
|
|
682
|
+
nil
|
|
683
|
+
when Hash
|
|
684
|
+
validate_schema_object!(schema, path)
|
|
685
|
+
schema.each do |key, child|
|
|
686
|
+
validate_meta_child!(key, child, join_pointer(path, key))
|
|
687
|
+
end
|
|
688
|
+
else
|
|
689
|
+
raise CompileError, "schema at #{path.empty? ? '/' : path} must be an object or boolean"
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def validate_meta_child!(key, child, path)
|
|
694
|
+
if DATA_VALUE_KEYWORDS.include?(key)
|
|
695
|
+
validate_data_keyword!(key, child, path)
|
|
696
|
+
elsif key == 'items' && child.is_a?(Array)
|
|
697
|
+
child.each_with_index { |subschema, index| validate_meta_schema!(subschema, join_pointer(path, index.to_s)) }
|
|
698
|
+
elsif SCHEMA_ARRAY_KEYWORDS.include?(key)
|
|
699
|
+
validate_schema_array!(key, child, path)
|
|
700
|
+
elsif SCHEMA_MAP_KEYWORDS.include?(key)
|
|
701
|
+
validate_schema_map!(key, child, path)
|
|
702
|
+
elsif SCHEMA_VALUE_KEYWORDS.include?(key) || %w[$ref $dynamicRef $recursiveRef].include?(key)
|
|
703
|
+
validate_meta_schema!(child, path) unless %w[$ref $dynamicRef $recursiveRef].include?(key)
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def validate_schema_object!(schema, path)
|
|
708
|
+
validate_type_keyword!(schema['type'], join_pointer(path, 'type')) if schema.key?('type')
|
|
709
|
+
validate_array_keyword!(schema, 'required', path, strings: true)
|
|
710
|
+
validate_array_keyword!(schema, 'enum', path)
|
|
711
|
+
validate_numeric_keyword!(schema, 'multipleOf', path, positive: true)
|
|
712
|
+
%w[maximum exclusiveMaximum minimum exclusiveMinimum].each do |keyword|
|
|
713
|
+
validate_numeric_keyword!(schema, keyword, path)
|
|
714
|
+
end
|
|
715
|
+
%w[maxLength minLength maxItems minItems maxProperties minProperties maxContains minContains].each do |keyword|
|
|
716
|
+
validate_non_negative_integer_keyword!(schema, keyword, path)
|
|
717
|
+
end
|
|
718
|
+
validate_boolean_keyword!(schema, 'uniqueItems', path)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def validate_data_keyword!(key, value, path)
|
|
722
|
+
return validate_array_value!(value, path, strings: true) if key == 'required'
|
|
723
|
+
return validate_schema_map_value!(value, path, arrays_of_strings: true) if key == 'dependentRequired'
|
|
724
|
+
return validate_array_value!(value, path) if key == 'enum'
|
|
725
|
+
|
|
726
|
+
true
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def validate_schema_array!(key, value, path)
|
|
730
|
+
validate_array_value!(value, path)
|
|
731
|
+
value.each_with_index { |subschema, index| validate_meta_schema!(subschema, join_pointer(path, index.to_s)) }
|
|
732
|
+
rescue NoMethodError
|
|
733
|
+
raise CompileError, "#{key} at #{path} must be an array"
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def validate_schema_map!(key, value, path)
|
|
737
|
+
validate_schema_map_value!(value, path)
|
|
738
|
+
value.each do |property, subschema|
|
|
739
|
+
validate_meta_schema!(subschema, join_pointer(path, property.to_s))
|
|
740
|
+
end
|
|
741
|
+
rescue NoMethodError
|
|
742
|
+
raise CompileError, "#{key} at #{path} must be an object"
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def validate_type_keyword!(value, path)
|
|
746
|
+
values = value.is_a?(Array) ? value : [value]
|
|
747
|
+
raise CompileError, "type at #{path} must be a string or array" unless value.is_a?(String) || value.is_a?(Array)
|
|
748
|
+
|
|
749
|
+
values.each do |type|
|
|
750
|
+
raise CompileError, "type value at #{path} must be a string" unless type.is_a?(String)
|
|
751
|
+
unless Instructions::TYPE_MAP.key?(type)
|
|
752
|
+
raise CompileError, "unsupported JSON Schema type at #{path}: #{type.inspect}"
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def validate_array_keyword!(schema, keyword, path, strings: false)
|
|
758
|
+
return unless schema.key?(keyword)
|
|
759
|
+
|
|
760
|
+
validate_array_value!(schema.fetch(keyword), join_pointer(path, keyword), strings: strings)
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def validate_array_value!(value, path, strings: false)
|
|
764
|
+
raise CompileError, "#{path} must be an array" unless value.is_a?(Array)
|
|
765
|
+
|
|
766
|
+
return unless strings
|
|
767
|
+
|
|
768
|
+
value.each do |item|
|
|
769
|
+
raise CompileError, "#{path} must contain only strings" unless item.is_a?(String)
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def validate_schema_map_value!(value, path, arrays_of_strings: false)
|
|
774
|
+
raise CompileError, "#{path} must be an object" unless value.is_a?(Hash)
|
|
775
|
+
|
|
776
|
+
return unless arrays_of_strings
|
|
777
|
+
|
|
778
|
+
value.each_value { |item| validate_array_value!(item, path, strings: true) }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def validate_numeric_keyword!(schema, keyword, path, positive: false)
|
|
782
|
+
return unless schema.key?(keyword)
|
|
783
|
+
|
|
784
|
+
value = schema.fetch(keyword)
|
|
785
|
+
raise CompileError, "#{keyword} at #{join_pointer(path, keyword)} must be numeric" unless value.is_a?(Numeric)
|
|
786
|
+
return unless positive && !value.positive?
|
|
787
|
+
|
|
788
|
+
raise CompileError,
|
|
789
|
+
"#{keyword} at #{join_pointer(path, keyword)} must be greater than 0"
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def validate_non_negative_integer_keyword!(schema, keyword, path)
|
|
793
|
+
return unless schema.key?(keyword)
|
|
794
|
+
|
|
795
|
+
value = schema.fetch(keyword)
|
|
796
|
+
return if value.is_a?(Integer) && value >= 0
|
|
797
|
+
|
|
798
|
+
raise CompileError, "#{keyword} at #{join_pointer(path, keyword)} must be a non-negative integer"
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def validate_boolean_keyword!(schema, keyword, path)
|
|
802
|
+
return unless schema.key?(keyword)
|
|
803
|
+
return if [true, false].include?(schema.fetch(keyword))
|
|
804
|
+
|
|
805
|
+
raise CompileError, "#{keyword} at #{join_pointer(path, keyword)} must be boolean"
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def normalize_schema_hash(schema, draft)
|
|
809
|
+
schema.each_with_object({}) do |(key, child), result|
|
|
810
|
+
result[key] = normalize_schema_child(key, child, draft)
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def normalize_schema_child(key, child, draft)
|
|
815
|
+
if DATA_VALUE_KEYWORDS.include?(key)
|
|
816
|
+
child
|
|
817
|
+
elsif key == 'items' && child.is_a?(Array)
|
|
818
|
+
child.map { |subschema| normalize_schema(subschema, draft) }
|
|
819
|
+
elsif SCHEMA_ARRAY_KEYWORDS.include?(key) && child.is_a?(Array)
|
|
820
|
+
child.map { |subschema| normalize_schema(subschema, draft) }
|
|
821
|
+
elsif SCHEMA_MAP_KEYWORDS.include?(key) && child.is_a?(Hash)
|
|
822
|
+
child.transform_values { |subschema| normalize_schema(subschema, draft) }
|
|
823
|
+
elsif SCHEMA_VALUE_KEYWORDS.include?(key)
|
|
824
|
+
normalize_schema(child, draft)
|
|
825
|
+
else
|
|
826
|
+
child
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def validation_vocabulary_enabled?(schema)
|
|
831
|
+
return true unless schema.is_a?(Hash) && schema['$schema'].is_a?(String)
|
|
832
|
+
|
|
833
|
+
meta_schema = (@options[:schemas] || {})[schema['$schema']]
|
|
834
|
+
return true unless meta_schema.is_a?(Hash)
|
|
835
|
+
|
|
836
|
+
vocabulary = meta_schema['$vocabulary'] || meta_schema[:$vocabulary]
|
|
837
|
+
return true unless vocabulary.is_a?(Hash)
|
|
838
|
+
|
|
839
|
+
vocabulary.fetch('https://json-schema.org/draft/2020-12/vocab/validation', false) == true ||
|
|
840
|
+
vocabulary.fetch('https://json-schema.org/draft/2019-09/vocab/validation', false) == true
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def format_assertion_enabled?
|
|
844
|
+
return true if @options[:format] == :assertion
|
|
845
|
+
return false unless @root_schema.is_a?(Hash) && @root_schema['$schema'].is_a?(String)
|
|
846
|
+
|
|
847
|
+
meta_schema = (@options[:schemas] || {})[@root_schema['$schema']]
|
|
848
|
+
return false unless meta_schema.is_a?(Hash)
|
|
849
|
+
|
|
850
|
+
vocabulary = meta_schema['$vocabulary'] || meta_schema[:$vocabulary]
|
|
851
|
+
return false unless vocabulary.is_a?(Hash)
|
|
852
|
+
|
|
853
|
+
vocabulary.key?('https://json-schema.org/draft/2020-12/vocab/format-assertion')
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def normalize_legacy_schema(schema, draft)
|
|
857
|
+
normalized = schema.dup
|
|
858
|
+
normalize_recursive_keywords(normalized)
|
|
859
|
+
normalize_legacy_id(normalized, draft)
|
|
860
|
+
normalize_legacy_exclusive_bounds(normalized, draft)
|
|
861
|
+
normalize_legacy_dependencies(normalized)
|
|
862
|
+
return normalized if draft == :'2020-12'
|
|
863
|
+
|
|
864
|
+
normalized.delete('prefixItems')
|
|
865
|
+
if normalized.key?('definitions') && !normalized.key?('$defs')
|
|
866
|
+
normalized['$defs'] = normalized.delete('definitions')
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
normalize_legacy_items(normalized)
|
|
870
|
+
normalized
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
def normalize_recursive_keywords(schema)
|
|
874
|
+
schema['$dynamicRef'] ||= schema.delete('$recursiveRef') if schema.key?('$recursiveRef')
|
|
875
|
+
schema['$dynamicAnchor'] ||= '' if schema.delete('$recursiveAnchor') == true
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def normalize_legacy_id(schema, draft)
|
|
879
|
+
return if draft == :'2020-12'
|
|
880
|
+
return unless schema.key?('id') && !schema.key?('$id')
|
|
881
|
+
|
|
882
|
+
schema['$id'] = schema.delete('id')
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def normalize_legacy_exclusive_bounds(schema, draft)
|
|
886
|
+
return unless draft == :'draft-04'
|
|
887
|
+
|
|
888
|
+
if schema.key?('exclusiveMaximum') && boolean_value?(schema['exclusiveMaximum'])
|
|
889
|
+
exclusive = schema.delete('exclusiveMaximum')
|
|
890
|
+
schema['exclusiveMaximum'] = schema.delete('maximum') if exclusive && schema.key?('maximum')
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
return unless schema.key?('exclusiveMinimum') && boolean_value?(schema['exclusiveMinimum'])
|
|
894
|
+
|
|
895
|
+
exclusive = schema.delete('exclusiveMinimum')
|
|
896
|
+
schema['exclusiveMinimum'] = schema.delete('minimum') if exclusive && schema.key?('minimum')
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def boolean_value?(value)
|
|
900
|
+
[true, false].include?(value)
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def normalize_openapi_nullable(schema)
|
|
904
|
+
return schema unless schema['nullable'] == true && schema.key?('type')
|
|
905
|
+
|
|
906
|
+
normalized = schema.dup
|
|
907
|
+
type = normalized['type']
|
|
908
|
+
normalized['type'] = Array(type).map(&:to_s).tap do |types|
|
|
909
|
+
types << 'null' unless types.include?('null')
|
|
910
|
+
end
|
|
911
|
+
normalized
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def normalize_legacy_items(schema)
|
|
915
|
+
return unless schema['items'].is_a?(Array)
|
|
916
|
+
|
|
917
|
+
schema['prefixItems'] ||= schema.delete('items')
|
|
918
|
+
schema['items'] = schema.delete('additionalItems') if schema.key?('additionalItems')
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def normalize_legacy_dependencies(schema)
|
|
922
|
+
dependencies = schema.delete('dependencies')
|
|
923
|
+
return unless dependencies.is_a?(Hash)
|
|
924
|
+
|
|
925
|
+
dependent_required = schema['dependentRequired'] ||= {}
|
|
926
|
+
dependent_schemas = schema['dependentSchemas'] ||= {}
|
|
927
|
+
|
|
928
|
+
dependencies.each do |key, value|
|
|
929
|
+
if value.is_a?(Array)
|
|
930
|
+
dependent_required[key] = value
|
|
931
|
+
else
|
|
932
|
+
dependent_schemas[key] = normalize_schema(value, @draft)
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def deep_stringify(value)
|
|
938
|
+
case value
|
|
939
|
+
when Hash
|
|
940
|
+
value.each_with_object({}) do |(key, child), result|
|
|
941
|
+
result[key.to_s] = deep_stringify(child)
|
|
942
|
+
end
|
|
943
|
+
when Array
|
|
944
|
+
value.map { |child| deep_stringify(child) }
|
|
945
|
+
else
|
|
946
|
+
value
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def numeric_value!(value, keyword)
|
|
951
|
+
return value if value.is_a?(Numeric)
|
|
952
|
+
|
|
953
|
+
raise CompileError, "#{keyword} must be numeric"
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def non_negative_integer!(value, keyword)
|
|
957
|
+
return value.to_i if integer_number?(value) && value >= 0
|
|
958
|
+
|
|
959
|
+
raise CompileError, "#{keyword} must be a non-negative integer"
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def integer_number?(value)
|
|
963
|
+
case value
|
|
964
|
+
when Integer
|
|
965
|
+
true
|
|
966
|
+
when Float, BigDecimal
|
|
967
|
+
value.finite? && value == value.to_i
|
|
968
|
+
else
|
|
969
|
+
false
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def instruction(op, payload, keyword_location, schema)
|
|
974
|
+
Instruction.new(op: op, payload: payload, keyword_location: keyword_location, schema: schema)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def keyword_to_op(keyword)
|
|
978
|
+
keyword.gsub(/([A-Z])/, '_\1').downcase.to_sym
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def join_pointer(base, *tokens)
|
|
982
|
+
tokens.reduce(base) do |memo, token|
|
|
983
|
+
"#{memo}/#{escape_pointer_token(token)}"
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def escape_pointer_token(token)
|
|
988
|
+
token.to_s.gsub('~', '~0').gsub('/', '~1')
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
end
|