schemurai 1.0.0 → 2.0.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.
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module VM
5
+ NumberRules = Data.define(
6
+ :mask,
7
+ :maximum,
8
+ :minimum,
9
+ :exclusive_maximum,
10
+ :exclusive_minimum,
11
+ :multiple_of
12
+ )
13
+
14
+ MAXIMUM = 1 << 0
15
+ MINIMUM = 1 << 1
16
+ EXCLUSIVE_MAXIMUM = 1 << 2
17
+ EXCLUSIVE_MINIMUM = 1 << 3
18
+ MULTIPLE_OF = 1 << 4
19
+
20
+ TypeRules = Data.define(:mask, :names)
21
+
22
+ TYPE_NULL = 1 << 0
23
+ TYPE_BOOLEAN = 1 << 1
24
+ TYPE_OBJECT = 1 << 2
25
+ TYPE_ARRAY = 1 << 3
26
+ TYPE_NUMBER = 1 << 4
27
+ TYPE_INTEGER = 1 << 5
28
+ TYPE_STRING = 1 << 6
29
+
30
+ TYPE_BITS = {
31
+ "null" => TYPE_NULL,
32
+ "boolean" => TYPE_BOOLEAN,
33
+ "object" => TYPE_OBJECT,
34
+ "array" => TYPE_ARRAY,
35
+ "number" => TYPE_NUMBER,
36
+ "integer" => TYPE_INTEGER,
37
+ "string" => TYPE_STRING
38
+ }.freeze
39
+
40
+ TYPE_OPCODES = {
41
+ "null" => :type_null,
42
+ "boolean" => :type_boolean,
43
+ "object" => :type_object,
44
+ "array" => :type_array,
45
+ "number" => :type_number,
46
+ "integer" => :type_integer,
47
+ "string" => :type_string
48
+ }.freeze
49
+
50
+ ReferenceRules = Data.define(:value, :fragment)
51
+
52
+ StringRules = Data.define(
53
+ :max_length,
54
+ :has_max_length,
55
+ :min_length,
56
+ :has_min_length,
57
+ :pattern,
58
+ :has_pattern,
59
+ :format,
60
+ :format_assertion,
61
+ :decode_base64,
62
+ :parse_json
63
+ )
64
+
65
+ ArrayRules = Data.define(
66
+ :max_items,
67
+ :has_max_items,
68
+ :min_items,
69
+ :has_min_items,
70
+ :unique,
71
+ :prefix_items,
72
+ :items,
73
+ :items_list,
74
+ :additional,
75
+ :contains,
76
+ :min_contains,
77
+ :max_contains,
78
+ :count_contains,
79
+ :unevaluated
80
+ )
81
+
82
+ ObjectRules = Data.define(
83
+ :max_properties,
84
+ :has_max_properties,
85
+ :min_properties,
86
+ :has_min_properties,
87
+ :required,
88
+ :has_required,
89
+ :properties,
90
+ :patterns,
91
+ :additional,
92
+ :property_names,
93
+ :dependencies,
94
+ :dependent_required,
95
+ :dependent_schemas,
96
+ :unevaluated
97
+ )
98
+
99
+ ConditionalRules = Data.define(
100
+ :condition,
101
+ :then_branch,
102
+ :else_branch,
103
+ :has_then,
104
+ :has_else
105
+ )
106
+
107
+ class Program
108
+ attr_reader :node, :code, :dynamic_anchor
109
+
110
+ def initialize(node)
111
+ @node = node
112
+ 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
116
+ end
117
+ end
118
+
119
+ 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
127
+ freeze
128
+ end
129
+
130
+ def tracks_evaluation?
131
+ @tracks_evaluation
132
+ end
133
+
134
+ def recursive_anchor?
135
+ @recursive_anchor
136
+ end
137
+
138
+ def tracks_dynamic_scope?
139
+ @tracks_dynamic_scope
140
+ 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
+ end
174
+
175
+ class Compiler
176
+ def initialize(graph)
177
+ @graph = graph
178
+ @programs = {}.compare_by_identity
179
+ end
180
+
181
+ 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
187
+ end
188
+
189
+ def compile_all
190
+ @graph.nodes.each { |node| compile(node) }
191
+ self
192
+ end
193
+
194
+ def resolve(program, reference)
195
+ compile(@graph.resolve(program.node, reference))
196
+ end
197
+
198
+ private def compile_code(node)
199
+ schema = node.schema
200
+ return [[:boolean, schema]] if schema == true || schema == false
201
+ return [] unless schema.is_a?(Hash)
202
+
203
+ code = []
204
+ if schema.key?("$ref")
205
+ code << [:ref, compile_reference(schema["$ref"])]
206
+ return code unless node.dialect.ref_siblings?
207
+ 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")
210
+
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")
217
+ 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)]
222
+ 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
+ code
226
+ end
227
+
228
+ private def compile_combiners(code, node, schema)
229
+ %w[allOf anyOf oneOf].each do |keyword|
230
+ next unless schema.key?(keyword)
231
+
232
+ children = schema[keyword].each_index.map { |index| compile(node.child(keyword, index)) }
233
+ code << [keyword.to_sym, children]
234
+ end
235
+ code << [:not, compile(node.child("not"))] if schema.key?("not")
236
+ return unless schema.key?("if")
237
+
238
+ code << [
239
+ :conditional,
240
+ 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")
246
+ )
247
+ ]
248
+ end
249
+
250
+ private def compile_number(schema)
251
+ mask = 0
252
+ mask |= MAXIMUM if schema.key?("maximum")
253
+ mask |= MINIMUM if schema.key?("minimum")
254
+ mask |= EXCLUSIVE_MAXIMUM if schema.key?("exclusiveMaximum")
255
+ mask |= EXCLUSIVE_MINIMUM if schema.key?("exclusiveMinimum")
256
+ mask |= MULTIPLE_OF if schema.key?("multipleOf")
257
+ 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"])
264
+ )
265
+ end
266
+
267
+ private def compile_reference(reference)
268
+ value = snapshot(reference)
269
+ separator = value.index("#")
270
+ fragment = separator ? value[(separator + 1)..].freeze : nil
271
+ ReferenceRules.new(value: value, fragment: fragment)
272
+ end
273
+
274
+ private def compile_type(types)
275
+ return [TYPE_OPCODES.fetch(types)] unless types.is_a?(Array)
276
+
277
+ mask = types.reduce(0) { |result, type| result | TYPE_BITS.fetch(type) }
278
+ [:types, TypeRules.new(mask: mask, names: snapshot(types))]
279
+ end
280
+
281
+ private def compile_string(node, schema)
282
+ StringRules.new(
283
+ schema["maxLength"],
284
+ schema.key?("maxLength"),
285
+ schema["minLength"],
286
+ schema.key?("minLength"),
287
+ snapshot(schema["pattern"]),
288
+ schema.key?("pattern"),
289
+ node.format,
290
+ node.dialect.format_assertion?,
291
+ schema["contentEncoding"] == "base64",
292
+ schema["contentMediaType"] == "application/json"
293
+ )
294
+ end
295
+
296
+ private def compile_array(node, schema)
297
+ prefix_items = if schema["prefixItems"].is_a?(Array)
298
+ schema["prefixItems"].each_index.map { |index| compile(node.child("prefixItems", index)) }.freeze
299
+ end
300
+ items = if schema["items"].is_a?(Array)
301
+ schema["items"].each_index.map { |index| compile(node.child("items", index)) }.freeze
302
+ elsif !schema["items"].nil?
303
+ compile(node.child("items"))
304
+ end
305
+ ArrayRules.new(
306
+ schema["maxItems"],
307
+ schema.key?("maxItems"),
308
+ schema["minItems"],
309
+ schema.key?("minItems"),
310
+ schema["uniqueItems"],
311
+ prefix_items,
312
+ items,
313
+ items.is_a?(Array),
314
+ schema.key?("additionalItems") ? compile(node.child("additionalItems")) : nil,
315
+ schema.key?("contains") ? compile(node.child("contains")) : nil,
316
+ schema.fetch("minContains", 1),
317
+ schema.fetch("maxContains", Float::INFINITY),
318
+ node.dialect.keywords.key?("minContains"),
319
+ schema.key?("unevaluatedItems") ? compile(node.child("unevaluatedItems")) : nil
320
+ )
321
+ end
322
+
323
+ private def compile_object(node, schema)
324
+ ObjectRules.new(
325
+ schema["maxProperties"],
326
+ schema.key?("maxProperties"),
327
+ schema["minProperties"],
328
+ schema.key?("minProperties"),
329
+ 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),
336
+ schema.fetch("dependentRequired", {}).map do |name, required_names|
337
+ [snapshot(name), snapshot(required_names)].freeze
338
+ end.freeze,
339
+ compile_map(node, schema, "dependentSchemas"),
340
+ schema.key?("unevaluatedProperties") ? compile(node.child("unevaluatedProperties")) : nil
341
+ )
342
+ end
343
+
344
+ private def compile_map(node, schema, keyword)
345
+ schema.fetch(keyword, {}).each_key.to_h do |name|
346
+ [snapshot(name), compile(node.child(keyword, name))]
347
+ end.freeze
348
+ end
349
+
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))
353
+ [snapshot(name), compiled].freeze
354
+ end.freeze
355
+ end
356
+
357
+ private def snapshot(value)
358
+ case value
359
+ when Hash
360
+ value.to_h { |key, item| [snapshot(key), snapshot(item)] }.freeze
361
+ when Array
362
+ value.map { |item| snapshot(item) }.freeze
363
+ when String
364
+ value.dup.freeze
365
+ else
366
+ value
367
+ end
368
+ end
369
+
370
+ private def compile_decimal(value)
371
+ return value if value.nil? || value.is_a?(Integer) || value.is_a?(Rational)
372
+
373
+ Rational(value.to_s)
374
+ end
375
+ end
376
+ end
377
+ end