senko 0.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 +7 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/Rakefile +56 -0
- data/ext/senko/extconf.rb +5 -0
- data/ext/senko/instructions.c +1 -0
- data/ext/senko/instructions.h +12 -0
- data/ext/senko/senko.c +17 -0
- data/ext/senko/validator.c +107 -0
- data/ext/senko/validator.h +14 -0
- data/lib/senko/cache.rb +57 -0
- data/lib/senko/code_generator.rb +270 -0
- data/lib/senko/compiler/instruction.rb +69 -0
- data/lib/senko/compiler/optimizer.rb +80 -0
- data/lib/senko/compiler/ref_resolver.rb +409 -0
- data/lib/senko/compiler.rb +991 -0
- data/lib/senko/dialect.rb +59 -0
- data/lib/senko/errors.rb +41 -0
- data/lib/senko/format.rb +327 -0
- data/lib/senko/native.rb +11 -0
- data/lib/senko/result.rb +65 -0
- data/lib/senko/schema.rb +58 -0
- data/lib/senko/validator.rb +1391 -0
- data/lib/senko/version.rb +5 -0
- data/lib/senko/vocabulary.rb +25 -0
- data/lib/senko.rb +171 -0
- data/sig/senko.rbs +45 -0
- metadata +170 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Senko
|
|
6
|
+
class Compiler
|
|
7
|
+
ResolvedRef = Struct.new(:schema, :keyword_location, :base_uri, :scope, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class RefResolver
|
|
10
|
+
ROOT_SCOPE = :__senko_root__
|
|
11
|
+
BUILTIN_METASCHEMA_URIS = [
|
|
12
|
+
'https://json-schema.org/draft/2020-12/schema',
|
|
13
|
+
'https://json-schema.org/draft/2020-12/schema#',
|
|
14
|
+
'https://json-schema.org/draft/2019-09/schema',
|
|
15
|
+
'https://json-schema.org/draft/2019-09/schema#',
|
|
16
|
+
'http://json-schema.org/draft-07/schema',
|
|
17
|
+
'http://json-schema.org/draft-07/schema#',
|
|
18
|
+
'http://json-schema.org/draft-06/schema',
|
|
19
|
+
'http://json-schema.org/draft-06/schema#',
|
|
20
|
+
'http://json-schema.org/draft-04/schema',
|
|
21
|
+
'http://json-schema.org/draft-04/schema#'
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(root_schema, schemas: {}, ref_resolver: nil)
|
|
25
|
+
@root_schema = root_schema
|
|
26
|
+
@schemas = {}
|
|
27
|
+
schemas.each do |uri, schema|
|
|
28
|
+
@schemas[uri.to_s] = registered_schema(uri.to_s, schema)
|
|
29
|
+
end
|
|
30
|
+
@ref_resolver = ref_resolver
|
|
31
|
+
@resources = {}
|
|
32
|
+
@locations = {}
|
|
33
|
+
@schema_scopes = {}
|
|
34
|
+
@resource_scopes = {}
|
|
35
|
+
@anchors = {}
|
|
36
|
+
@dynamic_anchors = {}
|
|
37
|
+
@base_by_location = {}
|
|
38
|
+
@scope_base = {}
|
|
39
|
+
index_schema(@root_schema, '', root_base_uri, ROOT_SCOPE)
|
|
40
|
+
index_registered_schemas
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve(uri, from: '', scope: ROOT_SCOPE)
|
|
44
|
+
uri = uri.to_s
|
|
45
|
+
base_uri = base_for(from, scope)
|
|
46
|
+
absolute = absolute_ref(uri, base_uri)
|
|
47
|
+
if absolute.empty? || absolute == '#'
|
|
48
|
+
return ResolvedRef.new(schema: @root_schema, keyword_location: '', base_uri: root_base_uri, scope: ROOT_SCOPE)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
base, fragment = split_fragment(absolute)
|
|
52
|
+
schema, target_scope = schema_for_base(base)
|
|
53
|
+
unless fragment
|
|
54
|
+
return ResolvedRef.new(
|
|
55
|
+
schema: schema,
|
|
56
|
+
keyword_location: location_for_schema(schema),
|
|
57
|
+
base_uri: base,
|
|
58
|
+
scope: target_scope
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
resolve_fragment(schema, fragment, base, target_scope)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_dynamic(uri, from: '', scope: ROOT_SCOPE)
|
|
66
|
+
uri = uri.to_s
|
|
67
|
+
base_uri = base_for(from, scope)
|
|
68
|
+
absolute = absolute_ref(uri, base_uri)
|
|
69
|
+
base, fragment = split_fragment(absolute)
|
|
70
|
+
anchor = URI::DEFAULT_PARSER.unescape(fragment.to_s)
|
|
71
|
+
|
|
72
|
+
if !anchor.empty? && !anchor.start_with?('/') && @dynamic_anchors.key?([base, anchor])
|
|
73
|
+
return @dynamic_anchors.fetch([base, anchor])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
resolve(uri, from: from, scope: scope)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def base_uri_for(location, scope)
|
|
80
|
+
base_for(location, scope)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dynamic_anchors_for(base_uri)
|
|
84
|
+
@dynamic_anchors.each_with_object({}) do |((base, anchor), resolved), result|
|
|
85
|
+
result[anchor] = resolved if base == base_uri
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def base_for(location, scope)
|
|
92
|
+
@base_by_location.fetch([scope, location]) { @scope_base.fetch(scope, root_base_uri) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def root_base_uri
|
|
96
|
+
schema_id = @root_schema.is_a?(Hash) ? @root_schema['$id'].to_s : ''
|
|
97
|
+
split_fragment(schema_id).first.to_s
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def absolute_ref(uri, base_uri)
|
|
101
|
+
return uri if uri.empty?
|
|
102
|
+
return "#{base_uri}#{uri}" if uri.start_with?('#') && !base_uri.to_s.empty?
|
|
103
|
+
return uri if uri.start_with?('#')
|
|
104
|
+
return uri if uri.match?(/\A[a-z][a-z0-9+\-.]*:/i)
|
|
105
|
+
return uri if base_uri.to_s.empty?
|
|
106
|
+
|
|
107
|
+
URI.join(base_uri, uri).to_s
|
|
108
|
+
rescue URI::InvalidURIError
|
|
109
|
+
uri
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def schema_for_base(base_uri)
|
|
113
|
+
return [@root_schema, ROOT_SCOPE] if base_uri.to_s.empty?
|
|
114
|
+
return [@resources[base_uri], @resource_scopes.fetch(base_uri)] if @resources.key?(base_uri)
|
|
115
|
+
if @schemas.key?(base_uri)
|
|
116
|
+
return [@schemas[base_uri],
|
|
117
|
+
@schema_scopes.fetch(@schemas[base_uri].object_id, base_uri)]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
schema = fetch_remote_schema(base_uri)
|
|
121
|
+
if schema
|
|
122
|
+
@schemas[base_uri] = schema
|
|
123
|
+
index_schema(schema, '', base_uri, base_uri)
|
|
124
|
+
return [schema, @schema_scopes.fetch(schema.object_id, base_uri)]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
raise UnresolvableRefError, "unable to resolve ref #{base_uri.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def fetch_remote_schema(uri)
|
|
131
|
+
return nil unless @ref_resolver
|
|
132
|
+
|
|
133
|
+
deep_stringify(@ref_resolver.call(uri))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def registered_schema(uri, schema)
|
|
137
|
+
return builtin_metaschema(uri) if schema == true && BUILTIN_METASCHEMA_URIS.include?(uri)
|
|
138
|
+
|
|
139
|
+
deep_stringify(schema)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def builtin_metaschema(uri)
|
|
143
|
+
{
|
|
144
|
+
'$id' => split_fragment(uri).first,
|
|
145
|
+
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
|
146
|
+
'type' => %w[object boolean],
|
|
147
|
+
'properties' => {
|
|
148
|
+
'type' => {
|
|
149
|
+
'anyOf' => [
|
|
150
|
+
{ 'enum' => json_type_names },
|
|
151
|
+
{ 'type' => 'array', 'items' => { 'enum' => json_type_names } }
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
'enum' => { 'type' => 'array' },
|
|
155
|
+
'const' => true,
|
|
156
|
+
'multipleOf' => { 'type' => 'number', 'exclusiveMinimum' => 0 },
|
|
157
|
+
'maximum' => { 'type' => 'number' },
|
|
158
|
+
'exclusiveMaximum' => { 'type' => 'number' },
|
|
159
|
+
'minimum' => { 'type' => 'number' },
|
|
160
|
+
'exclusiveMinimum' => { 'type' => 'number' },
|
|
161
|
+
'maxLength' => non_negative_integer_schema,
|
|
162
|
+
'minLength' => non_negative_integer_schema,
|
|
163
|
+
'pattern' => { 'type' => 'string' },
|
|
164
|
+
'maxItems' => non_negative_integer_schema,
|
|
165
|
+
'minItems' => non_negative_integer_schema,
|
|
166
|
+
'uniqueItems' => { 'type' => 'boolean' },
|
|
167
|
+
'maxContains' => non_negative_integer_schema,
|
|
168
|
+
'minContains' => non_negative_integer_schema,
|
|
169
|
+
'maxProperties' => non_negative_integer_schema,
|
|
170
|
+
'minProperties' => non_negative_integer_schema,
|
|
171
|
+
'required' => string_array_schema,
|
|
172
|
+
'$defs' => schema_map_schema,
|
|
173
|
+
'definitions' => schema_map_schema,
|
|
174
|
+
'properties' => schema_map_schema,
|
|
175
|
+
'patternProperties' => schema_map_schema,
|
|
176
|
+
'dependentRequired' => {
|
|
177
|
+
'type' => 'object',
|
|
178
|
+
'additionalProperties' => string_array_schema
|
|
179
|
+
},
|
|
180
|
+
'dependentSchemas' => schema_map_schema,
|
|
181
|
+
'propertyNames' => { '$ref' => '#' },
|
|
182
|
+
'items' => { '$ref' => '#' },
|
|
183
|
+
'additionalItems' => { '$ref' => '#' },
|
|
184
|
+
'additionalProperties' => { '$ref' => '#' },
|
|
185
|
+
'contains' => { '$ref' => '#' },
|
|
186
|
+
'not' => { '$ref' => '#' },
|
|
187
|
+
'if' => { '$ref' => '#' },
|
|
188
|
+
'then' => { '$ref' => '#' },
|
|
189
|
+
'else' => { '$ref' => '#' },
|
|
190
|
+
'unevaluatedItems' => { '$ref' => '#' },
|
|
191
|
+
'unevaluatedProperties' => { '$ref' => '#' },
|
|
192
|
+
'allOf' => schema_array_schema,
|
|
193
|
+
'anyOf' => schema_array_schema,
|
|
194
|
+
'oneOf' => schema_array_schema,
|
|
195
|
+
'prefixItems' => schema_array_schema
|
|
196
|
+
},
|
|
197
|
+
'additionalProperties' => true
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def json_type_names
|
|
202
|
+
%w[null boolean object array number string integer]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def non_negative_integer_schema
|
|
206
|
+
{ 'type' => 'integer', 'minimum' => 0 }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def string_array_schema
|
|
210
|
+
{ 'type' => 'array', 'items' => { 'type' => 'string' } }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def schema_array_schema
|
|
214
|
+
{ 'type' => 'array', 'items' => { '$ref' => '#' } }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def schema_map_schema
|
|
218
|
+
{ 'type' => 'object', 'additionalProperties' => { '$ref' => '#' } }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def split_fragment(uri)
|
|
222
|
+
index = uri.index('#')
|
|
223
|
+
return [uri, nil] unless index
|
|
224
|
+
|
|
225
|
+
[uri[0...index], uri[(index + 1)..]]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def resolve_fragment(schema, fragment, base_uri, scope)
|
|
229
|
+
decoded = URI::DEFAULT_PARSER.unescape(fragment.to_s)
|
|
230
|
+
if decoded.empty?
|
|
231
|
+
return ResolvedRef.new(
|
|
232
|
+
schema: schema,
|
|
233
|
+
keyword_location: location_for_schema(schema),
|
|
234
|
+
base_uri: base_uri,
|
|
235
|
+
scope: scope
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
return resolve_pointer(schema, decoded, base_uri, scope) if decoded.start_with?('/')
|
|
239
|
+
|
|
240
|
+
resolve_anchor(schema, decoded, base_uri, scope)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def resolve_pointer(schema, pointer, base_uri, scope)
|
|
244
|
+
current = schema
|
|
245
|
+
location = location_for_schema(schema)
|
|
246
|
+
|
|
247
|
+
pointer.split('/', -1)[1..].each do |raw_token|
|
|
248
|
+
token = unescape_pointer_token(raw_token)
|
|
249
|
+
current = pointer_child(current, token, pointer)
|
|
250
|
+
location = join_pointer(location, token)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
ResolvedRef.new(schema: current, keyword_location: location, base_uri: base_uri, scope: scope)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def pointer_child(current, token, pointer)
|
|
257
|
+
if current.is_a?(Hash)
|
|
258
|
+
raise UnresolvableRefError, "unable to resolve JSON Pointer #{pointer.inspect}" unless current.key?(token)
|
|
259
|
+
|
|
260
|
+
current[token]
|
|
261
|
+
elsif current.is_a?(Array)
|
|
262
|
+
index = Integer(token, exception: false)
|
|
263
|
+
unless index && index >= 0 && index < current.length
|
|
264
|
+
raise UnresolvableRefError,
|
|
265
|
+
"unable to resolve JSON Pointer #{pointer.inspect}"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
current[index]
|
|
269
|
+
else
|
|
270
|
+
raise UnresolvableRefError, "unable to resolve JSON Pointer #{pointer.inspect}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def resolve_anchor(schema, anchor, base_uri, scope)
|
|
275
|
+
found = @anchors[[base_uri, anchor]] || @anchors[['', anchor]]
|
|
276
|
+
return found if found
|
|
277
|
+
|
|
278
|
+
found = find_anchor(schema, anchor, location_for_schema(schema), base_uri, scope)
|
|
279
|
+
raise UnresolvableRefError, "unable to resolve anchor #{anchor.inspect}" unless found
|
|
280
|
+
|
|
281
|
+
found
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def find_anchor(value, anchor, location, base_uri, scope)
|
|
285
|
+
if value.is_a?(Hash)
|
|
286
|
+
if value['$anchor'] == anchor || value['$dynamicAnchor'] == anchor
|
|
287
|
+
return ResolvedRef.new(schema: value, keyword_location: location, base_uri: base_uri, scope: scope)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
value.each do |key, child|
|
|
291
|
+
found = find_anchor(child, anchor, join_pointer(location, key), base_uri, scope)
|
|
292
|
+
return found if found
|
|
293
|
+
end
|
|
294
|
+
elsif value.is_a?(Array)
|
|
295
|
+
value.each_with_index do |child, index|
|
|
296
|
+
found = find_anchor(child, anchor, join_pointer(location, index.to_s), base_uri, scope)
|
|
297
|
+
return found if found
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def index_registered_schemas
|
|
305
|
+
@schemas.each do |uri, schema|
|
|
306
|
+
scope = split_fragment(uri).first
|
|
307
|
+
index_schema(schema, '', scope, scope)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def index_schema(value, location, current_base, scope)
|
|
312
|
+
return unless value.is_a?(Hash)
|
|
313
|
+
|
|
314
|
+
@scope_base[scope] ||= current_base
|
|
315
|
+
next_base = schema_base_uri(value, current_base)
|
|
316
|
+
@base_by_location[[scope, location]] = next_base
|
|
317
|
+
if !next_base.to_s.empty? && (location.empty? || value.key?('$id'))
|
|
318
|
+
@resources[next_base] = value
|
|
319
|
+
@resource_scopes[next_base] = scope
|
|
320
|
+
end
|
|
321
|
+
@locations[value.object_id] = location
|
|
322
|
+
@schema_scopes[value.object_id] = scope
|
|
323
|
+
index_anchor(value, '$anchor', @anchors, location, next_base, scope)
|
|
324
|
+
index_anchor(value, '$dynamicAnchor', @dynamic_anchors, location, next_base, scope)
|
|
325
|
+
|
|
326
|
+
schema_children(value).each do |child_location, child|
|
|
327
|
+
index_schema(child, join_pointer(location, *Array(child_location)), next_base, scope)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def schema_children(schema)
|
|
332
|
+
children = []
|
|
333
|
+
schema.each do |key, child|
|
|
334
|
+
key = key.to_s
|
|
335
|
+
if schema_value_keyword?(key)
|
|
336
|
+
children << [key, child] if child.is_a?(Hash)
|
|
337
|
+
elsif schema_array_keyword?(key) && child.is_a?(Array)
|
|
338
|
+
child.each_with_index { |item, index| children << [[key, index.to_s], item] if item.is_a?(Hash) }
|
|
339
|
+
elsif schema_map_keyword?(key) && child.is_a?(Hash)
|
|
340
|
+
child.each { |name, item| children << [[key, name.to_s], item] if item.is_a?(Hash) }
|
|
341
|
+
elsif key == 'dependencies' && child.is_a?(Hash)
|
|
342
|
+
child.each { |name, item| children << [[key, name.to_s], item] if item.is_a?(Hash) }
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
children
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def schema_value_keyword?(key)
|
|
349
|
+
%w[
|
|
350
|
+
items additionalItems additionalProperties propertyNames contains not if then else
|
|
351
|
+
unevaluatedProperties unevaluatedItems
|
|
352
|
+
].include?(key)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def schema_array_keyword?(key)
|
|
356
|
+
%w[allOf anyOf oneOf prefixItems].include?(key)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def schema_map_keyword?(key)
|
|
360
|
+
%w[properties patternProperties $defs definitions dependentSchemas].include?(key)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def index_anchor(schema, keyword, index, location, base_uri, scope)
|
|
364
|
+
anchor = schema[keyword]
|
|
365
|
+
return unless anchor.is_a?(String)
|
|
366
|
+
|
|
367
|
+
index[[base_uri, anchor]] =
|
|
368
|
+
ResolvedRef.new(schema: schema, keyword_location: location, base_uri: base_uri, scope: scope)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def schema_base_uri(schema, current_base)
|
|
372
|
+
return current_base unless schema.key?('$id')
|
|
373
|
+
|
|
374
|
+
id = schema['$id'].to_s
|
|
375
|
+
resolved = current_base.to_s.empty? ? id : URI.join(current_base, id).to_s
|
|
376
|
+
split_fragment(resolved).first
|
|
377
|
+
rescue URI::InvalidURIError
|
|
378
|
+
id
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def location_for_schema(schema)
|
|
382
|
+
@locations.fetch(schema.object_id, '')
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def deep_stringify(value)
|
|
386
|
+
case value
|
|
387
|
+
when Hash
|
|
388
|
+
value.each_with_object({}) do |(key, child), result|
|
|
389
|
+
result[key.to_s] = deep_stringify(child)
|
|
390
|
+
end
|
|
391
|
+
when Array
|
|
392
|
+
value.map { |child| deep_stringify(child) }
|
|
393
|
+
else
|
|
394
|
+
value
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def unescape_pointer_token(token)
|
|
399
|
+
token.gsub('~1', '/').gsub('~0', '~')
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def join_pointer(base, *tokens)
|
|
403
|
+
tokens.reduce(base) do |memo, token|
|
|
404
|
+
"#{memo}/#{token.to_s.gsub('~', '~0').gsub('/', '~1')}"
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|