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.
- checksums.yaml +4 -4
- data/README.md +95 -3
- data/lib/schemurai/backend.rb +32 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluator.rb +30 -23
- data/lib/schemurai/meta_schemas/draft2019_09.rb +103 -2
- data/lib/schemurai/meta_schemas/draft2020_12.rb +103 -2
- data/lib/schemurai/schema_domain.rb +82 -0
- data/lib/schemurai/schema_graph.rb +21 -6
- data/lib/schemurai/version.rb +2 -1
- data/lib/schemurai/vm/compiler.rb +377 -0
- data/lib/schemurai/vm/evaluator.rb +811 -0
- data/lib/schemurai.rb +167 -18
- metadata +21 -5
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
|
356
|
+
compile_document(meta_schema, document_uri, dialect, changes)
|
|
342
357
|
end
|
|
343
358
|
(@indexed_external_schemas ||= {})[document_uri] = true
|
|
344
359
|
return
|
data/lib/schemurai/version.rb
CHANGED
|
@@ -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
|