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.
@@ -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
- @recursive_anchor = schema.is_a?(Hash) && schema["$recursiveAnchor"] == true
114
- @dynamic_anchor = if schema.is_a?(Hash) && schema["$dynamicAnchor"].is_a?(String)
115
- schema["$dynamicAnchor"].dup.freeze
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
- @code = code.map(&:freeze).freeze
121
- @tracks_evaluation = code.any? do |instruction|
122
- %i[array object].include?(instruction.first) && instruction[1].unevaluated
123
- end
124
- @tracks_dynamic_scope = code.any? do |opcode, operand|
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.fetch(node) do
183
- program = Program.new(node)
184
- @programs[node] = program
185
- program.finish(compile_code(node))
186
- end
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 compile_code(node)
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 [[:boolean, schema]] if schema == true || schema == false
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
- code << [:ref, compile_reference(schema["$ref"])]
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
- code << [:recursive_ref, compile_reference(schema["$recursiveRef"])] if schema.key?("$recursiveRef")
209
- code << [:dynamic_ref, compile_reference(schema["$dynamicRef"])] if schema.key?("$dynamicRef")
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
- categories = Schemurai.const_get(:Internal)::Dialect
213
- code << compile_type(schema["type"]) if (mask & categories::TYPE) != 0 && schema.key?("type")
214
- if (mask & categories::ENUM) != 0
215
- code << [:enum, snapshot(schema["enum"])] if schema.key?("enum")
216
- code << [:const, snapshot(schema["const"])] if schema.key?("const")
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
- compile_combiners(code, node, schema) if (mask & categories::COMBINER) != 0
219
- code << [:number, compile_number(schema)] if (mask & categories::NUMBER) != 0
220
- if (mask & categories::STRING) != 0 || node.format
221
- code << [:string, compile_string(node, schema)]
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 { |index| compile(node.child(keyword, index)) }
233
- code << [keyword.to_sym, children]
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 << [:not, compile(node.child("not"))] if schema.key?("not")
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
- code << [
270
+ program.emit(
271
+ code,
239
272
  :conditional,
240
273
  ConditionalRules.new(
241
- compile(node.child("if")),
242
- schema.key?("then") ? compile(node.child("then")) : nil,
243
- schema.key?("else") ? compile(node.child("else")) : nil,
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: mask,
259
- maximum: compile_decimal(schema["maximum"]),
260
- minimum: compile_decimal(schema["minimum"]),
261
- exclusive_maximum: compile_decimal(schema["exclusiveMaximum"]),
262
- exclusive_minimum: compile_decimal(schema["exclusiveMinimum"]),
263
- multiple_of: compile_decimal(schema["multipleOf"])
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)..].freeze : nil
271
- ReferenceRules.new(value: value, fragment: fragment)
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 [TYPE_OPCODES.fetch(types)] unless types.is_a?(Array)
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
- [:types, TypeRules.new(mask: mask, names: snapshot(types))]
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 { |index| compile(node.child("prefixItems", index)) }.freeze
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 { |index| compile(node.child("items", index)) }.freeze
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
- compile(node.child("items"))
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") ? compile(node.child("additionalItems")) : nil,
315
- schema.key?("contains") ? compile(node.child("contains")) : nil,
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") ? compile(node.child("unevaluatedItems")) : nil
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.key?("required"),
331
- compile_map(node, schema, "properties"),
332
- compile_map(node, schema, "patternProperties"),
333
- schema.key?("additionalProperties") ? compile(node.child("additionalProperties")) : nil,
334
- schema.key?("propertyNames") ? compile(node.child("propertyNames")) : nil,
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
- compile_map(node, schema, "dependentSchemas"),
340
- schema.key?("unevaluatedProperties") ? compile(node.child("unevaluatedProperties")) : nil
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), compile(node.child(keyword, name))]
373
+ [snapshot(name), compile_child(node.child(keyword, name), program)]
347
374
  end.freeze
348
375
  end
349
376
 
350
- private def compile_dependencies(node, schema)
351
- schema.fetch("dependencies", {}).map do |name, dependency|
352
- compiled = dependency.is_a?(Array) ? snapshot(dependency) : compile(node.child("dependencies", name))
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.freeze
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.dup.freeze
401
+ -value
365
402
  else
366
403
  value
367
404
  end