schemurai 1.0.0 → 2.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.
@@ -13,8 +13,46 @@ module Schemurai
13
13
  "$dynamicAnchor" => "meta",
14
14
  "type" => ["object", "boolean"],
15
15
  "properties" => {
16
+ "$id" => {"type" => "string"},
17
+ "$schema" => {"type" => "string"},
18
+ "$ref" => {"type" => "string"},
19
+ "$anchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
20
+ "$dynamicRef" => {"type" => "string"},
21
+ "$dynamicAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
22
+ "$recursiveRef" => {"type" => "string"},
23
+ "$recursiveAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
24
+ "$vocabulary" => {"type" => "object", "additionalProperties" => {"type" => "boolean"}},
25
+ "$comment" => {"type" => "string"},
16
26
  "$defs" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
17
27
  "definitions" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
28
+ "prefixItems" => {
29
+ "type" => "array",
30
+ "minItems" => 1,
31
+ "items" => {"$dynamicRef" => "#meta"}
32
+ },
33
+ "items" => {"$dynamicRef" => "#meta"},
34
+ "contains" => {"$dynamicRef" => "#meta"},
35
+ "additionalProperties" => {"$dynamicRef" => "#meta"},
36
+ "properties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
37
+ "patternProperties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
38
+ "dependentSchemas" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
39
+ "dependencies" => {
40
+ "type" => "object",
41
+ "additionalProperties" => {
42
+ "anyOf" => [
43
+ {"$dynamicRef" => "#meta"},
44
+ {"type" => "array", "items" => {"type" => "string"}, "uniqueItems" => true}
45
+ ]
46
+ }
47
+ },
48
+ "propertyNames" => {"$dynamicRef" => "#meta"},
49
+ "if" => {"$dynamicRef" => "#meta"},
50
+ "then" => {"$dynamicRef" => "#meta"},
51
+ "else" => {"$dynamicRef" => "#meta"},
52
+ "allOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
53
+ "anyOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
54
+ "oneOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
55
+ "not" => {"$dynamicRef" => "#meta"},
18
56
  "type" => {
19
57
  "anyOf" => [
20
58
  {"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
@@ -26,20 +64,83 @@ module Schemurai
26
64
  }
27
65
  ]
28
66
  },
67
+ "enum" => {"type" => "array"},
68
+ "multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
69
+ "maximum" => {"type" => "number"},
70
+ "exclusiveMaximum" => {"type" => "number"},
71
+ "minimum" => {"type" => "number"},
72
+ "exclusiveMinimum" => {"type" => "number"},
29
73
  "minLength" => {"type" => "integer", "minimum" => 0},
30
74
  "maxLength" => {"type" => "integer", "minimum" => 0},
75
+ "pattern" => {"type" => "string"},
31
76
  "minItems" => {"type" => "integer", "minimum" => 0},
32
77
  "maxItems" => {"type" => "integer", "minimum" => 0},
78
+ "uniqueItems" => {"type" => "boolean"},
33
79
  "minContains" => {"type" => "integer", "minimum" => 0},
34
80
  "maxContains" => {"type" => "integer", "minimum" => 0},
35
81
  "minProperties" => {"type" => "integer", "minimum" => 0},
36
- "maxProperties" => {"type" => "integer", "minimum" => 0}
82
+ "maxProperties" => {"type" => "integer", "minimum" => 0},
83
+ "required" => {
84
+ "type" => "array",
85
+ "items" => {"type" => "string"},
86
+ "uniqueItems" => true
87
+ },
88
+ "dependentRequired" => {
89
+ "type" => "object",
90
+ "additionalProperties" => {
91
+ "type" => "array",
92
+ "items" => {"type" => "string"},
93
+ "uniqueItems" => true
94
+ }
95
+ },
96
+ "title" => {"type" => "string"},
97
+ "description" => {"type" => "string"},
98
+ "deprecated" => {"type" => "boolean"},
99
+ "readOnly" => {"type" => "boolean"},
100
+ "writeOnly" => {"type" => "boolean"},
101
+ "examples" => {"type" => "array"},
102
+ "format" => {"type" => "string"},
103
+ "contentEncoding" => {"type" => "string"},
104
+ "contentMediaType" => {"type" => "string"},
105
+ "contentSchema" => {"$dynamicRef" => "#meta"}
37
106
  }
38
107
  }.freeze
39
108
 
40
109
  MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
41
110
 
42
- private_constant :META_SCHEMA_URI, :SCHEMA
111
+ COMPONENT_KEYWORDS = {
112
+ "core" => %w[
113
+ $id $schema $ref $anchor $dynamicRef $dynamicAnchor $vocabulary $comment $defs
114
+ ],
115
+ "applicator" => %w[
116
+ prefixItems items contains additionalProperties properties patternProperties
117
+ dependentSchemas propertyNames if then else allOf anyOf oneOf not
118
+ ],
119
+ "unevaluated" => %w[unevaluatedItems unevaluatedProperties],
120
+ "validation" => %w[
121
+ type const enum multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
122
+ maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
123
+ maxProperties minProperties required dependentRequired
124
+ ],
125
+ "meta-data" => %w[title description default deprecated readOnly writeOnly examples],
126
+ "format-annotation" => %w[format],
127
+ "format-assertion" => %w[format],
128
+ "content" => %w[contentEncoding contentMediaType contentSchema]
129
+ }.freeze
130
+
131
+ COMPONENT_KEYWORDS.each do |name, keywords|
132
+ uri = "https://json-schema.org/draft/2020-12/meta/#{name}"
133
+ component = {
134
+ "$schema" => META_SCHEMA_URI,
135
+ "$id" => uri,
136
+ "$dynamicAnchor" => "meta",
137
+ "type" => ["object", "boolean"],
138
+ "properties" => SCHEMA.fetch("properties").slice(*keywords)
139
+ }.freeze
140
+ MetaSchemas.register(uri, component)
141
+ end
142
+
143
+ private_constant :META_SCHEMA_URI, :SCHEMA, :COMPONENT_KEYWORDS
43
144
  end
44
145
  end
45
146
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module SchemaDomain
6
+ module_function def validate!(schema, label: "schema")
7
+ unless schema.equal?(true) || schema.equal?(false) || schema.instance_of?(Hash)
8
+ invalid!(label, "must be a boolean or an object")
9
+ end
10
+
11
+ validate_value!(schema, label, {})
12
+ schema
13
+ end
14
+
15
+ module_function def validate_registry!(schemas)
16
+ invalid!("schemas", "must be an object") unless schemas.instance_of?(Hash)
17
+
18
+ schemas.each do |uri, schema|
19
+ invalid!("schemas", "contains a non-string URI key") unless uri.instance_of?(String)
20
+
21
+ validate!(schema, label: "schemas[#{uri.inspect}]")
22
+ end
23
+ schemas
24
+ end
25
+
26
+ module_function def validate_value!(value, path, ancestors)
27
+ case value
28
+ when nil, true, false
29
+ nil
30
+ when String, Integer
31
+ invalid!(path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
32
+ when Float
33
+ invalid!(path, "must contain only finite numbers") unless value.finite?
34
+ invalid!(path, "uses a Float subclass") unless value.instance_of?(Float)
35
+ when Array
36
+ invalid!(path, "uses an Array subclass") unless value.instance_of?(Array)
37
+ descend!(value, path, ancestors) do
38
+ value.each_with_index { |item, index| validate_value!(item, pointer(path, index), ancestors) }
39
+ end
40
+ when Hash
41
+ invalid!(path, "uses a Hash subclass") unless value.instance_of?(Hash)
42
+ descend!(value, path, ancestors) do
43
+ value.each do |key, item|
44
+ invalid!(path, "contains a non-string object key") unless key.instance_of?(String)
45
+
46
+ validate_value!(item, pointer(path, key), ancestors)
47
+ end
48
+ end
49
+ else
50
+ invalid!(path, "contains unsupported #{value.class}")
51
+ end
52
+ end
53
+ private_class_method :validate_value!
54
+
55
+ module_function def supported_exact_scalar?(value)
56
+ value.instance_of?(String) || value.instance_of?(Integer)
57
+ end
58
+ private_class_method :supported_exact_scalar?
59
+
60
+ module_function def descend!(value, path, ancestors)
61
+ invalid!(path, "contains a recursive container") if ancestors.key?(value.object_id)
62
+
63
+ ancestors[value.object_id] = true
64
+ yield
65
+ ensure
66
+ ancestors.delete(value.object_id)
67
+ end
68
+ private_class_method :descend!
69
+
70
+ module_function def pointer(path, segment)
71
+ escaped = segment.to_s.gsub("~", "~0").gsub("/", "~1")
72
+ "#{path}/#{escaped}"
73
+ end
74
+ private_class_method :pointer
75
+
76
+ module_function def invalid!(path, reason)
77
+ raise Error, "invalid JSON-shaped #{path}: #{reason}"
78
+ end
79
+ private_class_method :invalid!
80
+ end
81
+ end
82
+ end
@@ -8,6 +8,7 @@ require_relative "meta_schemas/draft7"
8
8
  require_relative "meta_schemas/draft2019_09"
9
9
  require_relative "meta_schemas/draft2020_12"
10
10
  require_relative "schema_node"
11
+ require_relative "schema_domain"
11
12
 
12
13
  module Schemurai
13
14
  module Internal
@@ -43,9 +44,11 @@ module Schemurai
43
44
  attr_reader :resources, :uri_registry, :nodes
44
45
 
45
46
  def initialize(schemas: {}, dialect: Dialect.resolve)
47
+ SchemaDomain.validate_registry!(schemas)
46
48
  @external_schemas = schemas.dup
47
49
  @indexed_external_schemas = nil
48
50
  @compiled_roots = {}.compare_by_identity
51
+ @meta_schema_roots = {}
49
52
  @resources = {}
50
53
  @uri_registry = {}
51
54
  @nodes = []
@@ -58,10 +61,10 @@ module Schemurai
58
61
  # evaluators sharing this graph may reach the dynamic reference.
59
62
  @dynamic_scope = !schemas.empty?
60
63
  @default_dialect = dialect
61
- @shareable = false
62
64
  end
63
65
 
64
66
  def compile(schema, base_uri: nil, dialect: @default_dialect)
67
+ SchemaDomain.validate!(schema)
65
68
  roots = @compiled_roots.fetch(schema) { @compiled_roots[schema] = {} }
66
69
  cache_key = [base_uri.to_s, dialect]
67
70
  roots.fetch(cache_key) do
@@ -72,12 +75,24 @@ module Schemurai
72
75
  end
73
76
  end
74
77
 
78
+ def meta_schema_root(schema)
79
+ SchemaDomain.validate!(schema)
80
+ uri = if schema.is_a?(Hash) && schema.key?("$schema")
81
+ schema["$schema"].to_s
82
+ else
83
+ @default_dialect.uri
84
+ end
85
+ root = (@meta_schema_roots[uri] ||= compile({"$ref" => uri}))
86
+ resolve(root, uri)
87
+ root
88
+ end
89
+
75
90
  def resolve(node, reference)
76
91
  if (resolved = @resolved_refs&.[](node))&.key?(reference)
77
92
  return resolved[reference]
78
93
  end
79
94
 
80
- if @shareable
95
+ if Ractor.shareable?(self)
81
96
  raise ResolutionError, "reference #{reference.inspect} was not resolved before sharing"
82
97
  end
83
98
 
@@ -94,7 +109,7 @@ module Schemurai
94
109
  end
95
110
 
96
111
  def make_shareable
97
- return self if @shareable
112
+ return self if Ractor.shareable?(self)
98
113
 
99
114
  @external_schemas.each_key do |uri|
100
115
  index_external(strip_fragment(uri.to_s), @default_dialect)
@@ -113,7 +128,6 @@ module Schemurai
113
128
  end
114
129
 
115
130
  @resolved_refs ||= {}
116
- @shareable = true
117
131
  Ractor.make_shareable(self)
118
132
  end
119
133
 
@@ -336,9 +350,10 @@ module Schemurai
336
350
  private def index_external(document_uri, fallback_dialect)
337
351
  return if @indexed_external_schemas&.[](document_uri)
338
352
 
339
- if (dialect = Dialect.resolve(document_uri)) && (meta_schema = MetaSchemas.resolve(document_uri))
353
+ if (meta_schema = MetaSchemas.resolve(document_uri))
354
+ dialect = Dialect.resolve(meta_schema["$schema"]) || fallback_dialect
340
355
  compile_atomically do |changes|
341
- compile_document(meta_schema, dialect.uri, dialect, changes)
356
+ compile_document(meta_schema, document_uri, dialect, changes)
342
357
  end
343
358
  (@indexed_external_schemas ||= {})[document_uri] = true
344
359
  return
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Schemurai
4
- VERSION = "1.0.0"
4
+ # The installed Schemurai version.
5
+ VERSION = "2.1.0"
5
6
  end
@@ -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