schemurai 2.0.0 → 2.2.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 +4 -4
- data/README.md +40 -2
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +19 -2
- data/lib/schemurai/evaluator.rb +24 -21
- data/lib/schemurai/schema_domain.rb +27 -19
- data/lib/schemurai/schema_graph.rb +70 -37
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +166 -129
- data/lib/schemurai/vm/evaluator.rb +523 -221
- data/lib/schemurai.rb +6 -3
- data/sig/schemurai.rbs +157 -0
- metadata +4 -2
|
@@ -27,6 +27,19 @@ module Schemurai
|
|
|
27
27
|
TYPE_INTEGER = 1 << 5
|
|
28
28
|
TYPE_STRING = 1 << 6
|
|
29
29
|
|
|
30
|
+
TRACKS_EVALUATION = 1 << 0
|
|
31
|
+
TRACKS_DYNAMIC_SCOPE = 1 << 1
|
|
32
|
+
|
|
33
|
+
DIALECT = Schemurai.const_get(:Internal)::Dialect
|
|
34
|
+
KEYWORD_TYPE = DIALECT::TYPE
|
|
35
|
+
KEYWORD_ENUM = DIALECT::ENUM
|
|
36
|
+
KEYWORD_COMBINER = DIALECT::COMBINER
|
|
37
|
+
KEYWORD_NUMBER = DIALECT::NUMBER
|
|
38
|
+
KEYWORD_STRING = DIALECT::STRING
|
|
39
|
+
KEYWORD_ARRAY = DIALECT::ARRAY
|
|
40
|
+
KEYWORD_OBJECT = DIALECT::OBJECT
|
|
41
|
+
private_constant :DIALECT
|
|
42
|
+
|
|
30
43
|
TYPE_BITS = {
|
|
31
44
|
"null" => TYPE_NULL,
|
|
32
45
|
"boolean" => TYPE_BOOLEAN,
|
|
@@ -51,11 +64,8 @@ module Schemurai
|
|
|
51
64
|
|
|
52
65
|
StringRules = Data.define(
|
|
53
66
|
:max_length,
|
|
54
|
-
:has_max_length,
|
|
55
67
|
:min_length,
|
|
56
|
-
:has_min_length,
|
|
57
68
|
:pattern,
|
|
58
|
-
:has_pattern,
|
|
59
69
|
:format,
|
|
60
70
|
:format_assertion,
|
|
61
71
|
:decode_base64,
|
|
@@ -64,9 +74,7 @@ module Schemurai
|
|
|
64
74
|
|
|
65
75
|
ArrayRules = Data.define(
|
|
66
76
|
:max_items,
|
|
67
|
-
:has_max_items,
|
|
68
77
|
:min_items,
|
|
69
|
-
:has_min_items,
|
|
70
78
|
:unique,
|
|
71
79
|
:prefix_items,
|
|
72
80
|
:items,
|
|
@@ -81,11 +89,8 @@ module Schemurai
|
|
|
81
89
|
|
|
82
90
|
ObjectRules = Data.define(
|
|
83
91
|
:max_properties,
|
|
84
|
-
:has_max_properties,
|
|
85
92
|
:min_properties,
|
|
86
|
-
:has_min_properties,
|
|
87
93
|
:required,
|
|
88
|
-
:has_required,
|
|
89
94
|
:properties,
|
|
90
95
|
:patterns,
|
|
91
96
|
:additional,
|
|
@@ -99,9 +104,7 @@ module Schemurai
|
|
|
99
104
|
ConditionalRules = Data.define(
|
|
100
105
|
:condition,
|
|
101
106
|
:then_branch,
|
|
102
|
-
:else_branch
|
|
103
|
-
:has_then,
|
|
104
|
-
:has_else
|
|
107
|
+
:else_branch
|
|
105
108
|
)
|
|
106
109
|
|
|
107
110
|
class Program
|
|
@@ -109,21 +112,36 @@ module Schemurai
|
|
|
109
112
|
|
|
110
113
|
def initialize(node)
|
|
111
114
|
@node = node
|
|
115
|
+
@tracks_evaluation = false
|
|
116
|
+
@tracks_dynamic_scope = false
|
|
112
117
|
schema = node.schema
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
schema["$dynamicAnchor"]
|
|
118
|
+
if schema.instance_of?(Hash)
|
|
119
|
+
@recursive_anchor = schema["$recursiveAnchor"] == true
|
|
120
|
+
dynamic_anchor = schema["$dynamicAnchor"]
|
|
121
|
+
@dynamic_anchor = -dynamic_anchor if dynamic_anchor.instance_of?(String)
|
|
122
|
+
else
|
|
123
|
+
@recursive_anchor = false
|
|
116
124
|
end
|
|
117
125
|
end
|
|
118
126
|
|
|
127
|
+
def emit(code, opcode, operand)
|
|
128
|
+
code.push(opcode, operand)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def track_evaluation!
|
|
132
|
+
@tracks_evaluation = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def track_dynamic_scope!
|
|
136
|
+
@tracks_dynamic_scope = true
|
|
137
|
+
end
|
|
138
|
+
|
|
119
139
|
def finish(code)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@
|
|
125
|
-
opcode == :ref || opcode == :recursive_ref || opcode == :dynamic_ref || dynamic_scope_operand?(operand)
|
|
126
|
-
end
|
|
140
|
+
flags = 0
|
|
141
|
+
flags |= TRACKS_EVALUATION if @tracks_evaluation
|
|
142
|
+
flags |= TRACKS_DYNAMIC_SCOPE if @tracks_dynamic_scope
|
|
143
|
+
code[0] = flags
|
|
144
|
+
@code = code.freeze
|
|
127
145
|
freeze
|
|
128
146
|
end
|
|
129
147
|
|
|
@@ -138,38 +156,6 @@ module Schemurai
|
|
|
138
156
|
def tracks_dynamic_scope?
|
|
139
157
|
@tracks_dynamic_scope
|
|
140
158
|
end
|
|
141
|
-
|
|
142
|
-
private def dynamic_scope_operand?(operand)
|
|
143
|
-
case operand
|
|
144
|
-
when Program
|
|
145
|
-
operand.tracks_dynamic_scope?
|
|
146
|
-
when Array
|
|
147
|
-
operand.any? { |item| dynamic_scope_operand?(item) }
|
|
148
|
-
when Hash
|
|
149
|
-
operand.each_value { |item| return true if dynamic_scope_operand?(item) }
|
|
150
|
-
false
|
|
151
|
-
when ArrayRules
|
|
152
|
-
dynamic_scope_operand?(operand.prefix_items) ||
|
|
153
|
-
dynamic_scope_operand?(operand.items) ||
|
|
154
|
-
dynamic_scope_operand?(operand.additional) ||
|
|
155
|
-
dynamic_scope_operand?(operand.contains) ||
|
|
156
|
-
dynamic_scope_operand?(operand.unevaluated)
|
|
157
|
-
when ObjectRules
|
|
158
|
-
dynamic_scope_operand?(operand.properties) ||
|
|
159
|
-
dynamic_scope_operand?(operand.patterns) ||
|
|
160
|
-
dynamic_scope_operand?(operand.additional) ||
|
|
161
|
-
dynamic_scope_operand?(operand.property_names) ||
|
|
162
|
-
dynamic_scope_operand?(operand.dependencies) ||
|
|
163
|
-
dynamic_scope_operand?(operand.dependent_schemas) ||
|
|
164
|
-
dynamic_scope_operand?(operand.unevaluated)
|
|
165
|
-
when ConditionalRules
|
|
166
|
-
dynamic_scope_operand?(operand.condition) ||
|
|
167
|
-
dynamic_scope_operand?(operand.then_branch) ||
|
|
168
|
-
dynamic_scope_operand?(operand.else_branch)
|
|
169
|
-
else
|
|
170
|
-
false
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
159
|
end
|
|
174
160
|
|
|
175
161
|
class Compiler
|
|
@@ -179,11 +165,12 @@ module Schemurai
|
|
|
179
165
|
end
|
|
180
166
|
|
|
181
167
|
def compile(node)
|
|
182
|
-
@programs
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
168
|
+
program = @programs[node]
|
|
169
|
+
return program if program
|
|
170
|
+
|
|
171
|
+
program = Program.new(node)
|
|
172
|
+
@programs[node] = program
|
|
173
|
+
program.finish(compile_code(node, program))
|
|
187
174
|
end
|
|
188
175
|
|
|
189
176
|
def compile_all
|
|
@@ -195,56 +182,100 @@ module Schemurai
|
|
|
195
182
|
compile(@graph.resolve(program.node, reference))
|
|
196
183
|
end
|
|
197
184
|
|
|
198
|
-
private def
|
|
185
|
+
private def compile_child(node, parent)
|
|
186
|
+
child = compile(node)
|
|
187
|
+
parent.track_dynamic_scope! if child.tracks_dynamic_scope?
|
|
188
|
+
child
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private def compile_code(node, program)
|
|
199
192
|
schema = node.schema
|
|
200
|
-
return [
|
|
201
|
-
return [] unless schema.is_a?(Hash)
|
|
193
|
+
return program.emit([0], :boolean, schema) if schema.equal?(true) || schema.equal?(false)
|
|
194
|
+
return [0] unless schema.is_a?(Hash)
|
|
202
195
|
|
|
203
|
-
code = []
|
|
196
|
+
code = [0]
|
|
204
197
|
if schema.key?("$ref")
|
|
205
|
-
|
|
198
|
+
program.track_dynamic_scope!
|
|
199
|
+
program.emit(code, :ref, compile_reference(schema["$ref"]))
|
|
206
200
|
return code unless node.dialect.ref_siblings?
|
|
207
201
|
end
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
if schema.key?("$recursiveRef")
|
|
203
|
+
program.track_dynamic_scope!
|
|
204
|
+
program.emit(code, :recursive_ref, compile_reference(schema["$recursiveRef"]))
|
|
205
|
+
end
|
|
206
|
+
if schema.key?("$dynamicRef")
|
|
207
|
+
program.track_dynamic_scope!
|
|
208
|
+
program.emit(code, :dynamic_ref, compile_reference(schema["$dynamicRef"]))
|
|
209
|
+
end
|
|
210
210
|
|
|
211
211
|
mask = node.keyword_mask
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
code
|
|
216
|
-
|
|
212
|
+
compile_type(code, program, schema["type"]) if (mask & KEYWORD_TYPE) != 0 && schema.key?("type")
|
|
213
|
+
if (mask & KEYWORD_ENUM) != 0
|
|
214
|
+
program.emit(code, :enum, snapshot(schema["enum"])) if schema.key?("enum")
|
|
215
|
+
program.emit(code, :const, snapshot(schema["const"])) if schema.key?("const")
|
|
216
|
+
end
|
|
217
|
+
compile_combiners(code, program, node, schema) if (mask & KEYWORD_COMBINER) != 0
|
|
218
|
+
program.emit(code, :number, compile_number(schema)) if (mask & KEYWORD_NUMBER) != 0
|
|
219
|
+
if (mask & KEYWORD_STRING) != 0 || node.format
|
|
220
|
+
program.emit(code, :string, compile_string(node, schema))
|
|
221
|
+
end
|
|
222
|
+
if (mask & KEYWORD_ARRAY) != 0
|
|
223
|
+
rules = compile_array(node, schema, program)
|
|
224
|
+
program.track_evaluation! if rules.unevaluated
|
|
225
|
+
program.emit(code, :array, rules)
|
|
217
226
|
end
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
code
|
|
227
|
+
if (mask & KEYWORD_OBJECT) != 0
|
|
228
|
+
rules = compile_object(node, schema, program)
|
|
229
|
+
program.track_evaluation! if rules.unevaluated
|
|
230
|
+
program.emit(code, :object, rules)
|
|
231
|
+
end
|
|
232
|
+
fuse_type_instruction(code)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private def fuse_type_instruction(code)
|
|
236
|
+
index = 1
|
|
237
|
+
while index + 3 < code.length
|
|
238
|
+
type_opcode = code[index]
|
|
239
|
+
rules_opcode = code[index + 2]
|
|
240
|
+
fused_opcode = case type_opcode
|
|
241
|
+
when :type_object then :typed_object if rules_opcode == :object
|
|
242
|
+
when :type_array then :typed_array if rules_opcode == :array
|
|
243
|
+
when :type_string then :typed_string if rules_opcode == :string
|
|
244
|
+
when :type_number then :typed_number if rules_opcode == :number
|
|
245
|
+
when :type_integer then :typed_integer if rules_opcode == :number
|
|
246
|
+
end
|
|
247
|
+
if fused_opcode
|
|
248
|
+
code[index] = fused_opcode
|
|
249
|
+
code[index + 1] = code[index + 3]
|
|
250
|
+
code.slice!(index + 2, 2)
|
|
251
|
+
break
|
|
252
|
+
end
|
|
253
|
+
index += 2
|
|
222
254
|
end
|
|
223
|
-
code << [:array, compile_array(node, schema)] if (mask & categories::ARRAY) != 0
|
|
224
|
-
code << [:object, compile_object(node, schema)] if (mask & categories::OBJECT) != 0
|
|
225
255
|
code
|
|
226
256
|
end
|
|
227
257
|
|
|
228
|
-
private def compile_combiners(code, node, schema)
|
|
258
|
+
private def compile_combiners(code, program, node, schema)
|
|
229
259
|
%w[allOf anyOf oneOf].each do |keyword|
|
|
230
260
|
next unless schema.key?(keyword)
|
|
231
261
|
|
|
232
|
-
children = schema[keyword].each_index.map
|
|
233
|
-
|
|
262
|
+
children = schema[keyword].each_index.map do |index|
|
|
263
|
+
compile_child(node.child(keyword, index), program)
|
|
264
|
+
end
|
|
265
|
+
program.emit(code, keyword.to_sym, children)
|
|
234
266
|
end
|
|
235
|
-
code
|
|
267
|
+
program.emit(code, :not, compile_child(node.child("not"), program)) if schema.key?("not")
|
|
236
268
|
return unless schema.key?("if")
|
|
237
269
|
|
|
238
|
-
|
|
270
|
+
program.emit(
|
|
271
|
+
code,
|
|
239
272
|
:conditional,
|
|
240
273
|
ConditionalRules.new(
|
|
241
|
-
|
|
242
|
-
schema.key?("then") ?
|
|
243
|
-
schema.key?("else") ?
|
|
244
|
-
schema.key?("then"),
|
|
245
|
-
schema.key?("else")
|
|
274
|
+
compile_child(node.child("if"), program),
|
|
275
|
+
schema.key?("then") ? compile_child(node.child("then"), program) : nil,
|
|
276
|
+
schema.key?("else") ? compile_child(node.child("else"), program) : nil
|
|
246
277
|
)
|
|
247
|
-
|
|
278
|
+
)
|
|
248
279
|
end
|
|
249
280
|
|
|
250
281
|
private def compile_number(schema)
|
|
@@ -255,37 +286,34 @@ module Schemurai
|
|
|
255
286
|
mask |= EXCLUSIVE_MINIMUM if schema.key?("exclusiveMinimum")
|
|
256
287
|
mask |= MULTIPLE_OF if schema.key?("multipleOf")
|
|
257
288
|
NumberRules.new(
|
|
258
|
-
mask
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
mask,
|
|
290
|
+
compile_decimal(schema["maximum"]),
|
|
291
|
+
compile_decimal(schema["minimum"]),
|
|
292
|
+
compile_decimal(schema["exclusiveMaximum"]),
|
|
293
|
+
compile_decimal(schema["exclusiveMinimum"]),
|
|
294
|
+
compile_decimal(schema["multipleOf"])
|
|
264
295
|
)
|
|
265
296
|
end
|
|
266
297
|
|
|
267
298
|
private def compile_reference(reference)
|
|
268
299
|
value = snapshot(reference)
|
|
269
300
|
separator = value.index("#")
|
|
270
|
-
fragment = separator ? value[(separator + 1)..]
|
|
271
|
-
ReferenceRules.new(value
|
|
301
|
+
fragment = separator ? -value[(separator + 1)..] : nil
|
|
302
|
+
ReferenceRules.new(value, fragment)
|
|
272
303
|
end
|
|
273
304
|
|
|
274
|
-
private def compile_type(types)
|
|
275
|
-
return
|
|
305
|
+
private def compile_type(code, program, types)
|
|
306
|
+
return program.emit(code, TYPE_OPCODES.fetch(types), nil) unless types.is_a?(Array)
|
|
276
307
|
|
|
277
308
|
mask = types.reduce(0) { |result, type| result | TYPE_BITS.fetch(type) }
|
|
278
|
-
|
|
309
|
+
program.emit(code, :types, TypeRules.new(mask, snapshot(types)))
|
|
279
310
|
end
|
|
280
311
|
|
|
281
312
|
private def compile_string(node, schema)
|
|
282
313
|
StringRules.new(
|
|
283
314
|
schema["maxLength"],
|
|
284
|
-
schema.key?("maxLength"),
|
|
285
315
|
schema["minLength"],
|
|
286
|
-
schema.key?("minLength"),
|
|
287
316
|
snapshot(schema["pattern"]),
|
|
288
|
-
schema.key?("pattern"),
|
|
289
317
|
node.format,
|
|
290
318
|
node.dialect.format_assertion?,
|
|
291
319
|
schema["contentEncoding"] == "base64",
|
|
@@ -293,65 +321,74 @@ module Schemurai
|
|
|
293
321
|
)
|
|
294
322
|
end
|
|
295
323
|
|
|
296
|
-
private def compile_array(node, schema)
|
|
324
|
+
private def compile_array(node, schema, program)
|
|
297
325
|
prefix_items = if schema["prefixItems"].is_a?(Array)
|
|
298
|
-
schema["prefixItems"].each_index.map
|
|
326
|
+
schema["prefixItems"].each_index.map do |index|
|
|
327
|
+
compile_child(node.child("prefixItems", index), program)
|
|
328
|
+
end.freeze
|
|
299
329
|
end
|
|
300
330
|
items = if schema["items"].is_a?(Array)
|
|
301
|
-
schema["items"].each_index.map
|
|
331
|
+
schema["items"].each_index.map do |index|
|
|
332
|
+
compile_child(node.child("items", index), program)
|
|
333
|
+
end.freeze
|
|
302
334
|
elsif !schema["items"].nil?
|
|
303
|
-
|
|
335
|
+
compile_child(node.child("items"), program)
|
|
304
336
|
end
|
|
305
337
|
ArrayRules.new(
|
|
306
338
|
schema["maxItems"],
|
|
307
|
-
schema.key?("maxItems"),
|
|
308
339
|
schema["minItems"],
|
|
309
|
-
schema.key?("minItems"),
|
|
310
340
|
schema["uniqueItems"],
|
|
311
341
|
prefix_items,
|
|
312
342
|
items,
|
|
313
343
|
items.is_a?(Array),
|
|
314
|
-
schema.key?("additionalItems") ?
|
|
315
|
-
schema.key?("contains") ?
|
|
344
|
+
schema.key?("additionalItems") ? compile_child(node.child("additionalItems"), program) : nil,
|
|
345
|
+
schema.key?("contains") ? compile_child(node.child("contains"), program) : nil,
|
|
316
346
|
schema.fetch("minContains", 1),
|
|
317
347
|
schema.fetch("maxContains", Float::INFINITY),
|
|
318
348
|
node.dialect.keywords.key?("minContains"),
|
|
319
|
-
schema.key?("unevaluatedItems") ?
|
|
349
|
+
schema.key?("unevaluatedItems") ? compile_child(node.child("unevaluatedItems"), program) : nil
|
|
320
350
|
)
|
|
321
351
|
end
|
|
322
352
|
|
|
323
|
-
private def compile_object(node, schema)
|
|
353
|
+
private def compile_object(node, schema, program)
|
|
324
354
|
ObjectRules.new(
|
|
325
355
|
schema["maxProperties"],
|
|
326
|
-
schema.key?("maxProperties"),
|
|
327
356
|
schema["minProperties"],
|
|
328
|
-
schema.key?("minProperties"),
|
|
329
357
|
snapshot(schema["required"]),
|
|
330
|
-
schema
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
schema.key?("
|
|
334
|
-
|
|
335
|
-
compile_dependencies(node, schema),
|
|
358
|
+
compile_map(node, schema, "properties", program),
|
|
359
|
+
compile_optional_map(node, schema, "patternProperties", program),
|
|
360
|
+
schema.key?("additionalProperties") ? compile_child(node.child("additionalProperties"), program) : nil,
|
|
361
|
+
schema.key?("propertyNames") ? compile_child(node.child("propertyNames"), program) : nil,
|
|
362
|
+
compile_dependencies(node, schema, program),
|
|
336
363
|
schema.fetch("dependentRequired", {}).map do |name, required_names|
|
|
337
364
|
[snapshot(name), snapshot(required_names)].freeze
|
|
338
|
-
end.freeze,
|
|
339
|
-
|
|
340
|
-
schema.key?("unevaluatedProperties") ?
|
|
365
|
+
end.then { |entries| entries.empty? ? nil : entries.freeze },
|
|
366
|
+
compile_optional_map(node, schema, "dependentSchemas", program),
|
|
367
|
+
schema.key?("unevaluatedProperties") ? compile_child(node.child("unevaluatedProperties"), program) : nil
|
|
341
368
|
)
|
|
342
369
|
end
|
|
343
370
|
|
|
344
|
-
private def compile_map(node, schema, keyword)
|
|
371
|
+
private def compile_map(node, schema, keyword, program)
|
|
345
372
|
schema.fetch(keyword, {}).each_key.to_h do |name|
|
|
346
|
-
[snapshot(name),
|
|
373
|
+
[snapshot(name), compile_child(node.child(keyword, name), program)]
|
|
347
374
|
end.freeze
|
|
348
375
|
end
|
|
349
376
|
|
|
350
|
-
private def
|
|
351
|
-
|
|
352
|
-
|
|
377
|
+
private def compile_optional_map(node, schema, keyword, program)
|
|
378
|
+
compiled = compile_map(node, schema, keyword, program)
|
|
379
|
+
compiled unless compiled.empty?
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private def compile_dependencies(node, schema, program)
|
|
383
|
+
compiled = schema.fetch("dependencies", {}).map do |name, dependency|
|
|
384
|
+
compiled = if dependency.is_a?(Array)
|
|
385
|
+
snapshot(dependency)
|
|
386
|
+
else
|
|
387
|
+
compile_child(node.child("dependencies", name), program)
|
|
388
|
+
end
|
|
353
389
|
[snapshot(name), compiled].freeze
|
|
354
|
-
end
|
|
390
|
+
end
|
|
391
|
+
compiled.freeze unless compiled.empty?
|
|
355
392
|
end
|
|
356
393
|
|
|
357
394
|
private def snapshot(value)
|
|
@@ -361,7 +398,7 @@ module Schemurai
|
|
|
361
398
|
when Array
|
|
362
399
|
value.map { |item| snapshot(item) }.freeze
|
|
363
400
|
when String
|
|
364
|
-
value
|
|
401
|
+
-value
|
|
365
402
|
else
|
|
366
403
|
value
|
|
367
404
|
end
|