jsi-dev 0.0.0.pre.commonmarker
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/.yardopts +8 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.md +613 -0
- data/README.md +303 -0
- data/docs/glossary.md +281 -0
- data/jsi.gemspec +30 -0
- data/lib/jsi/base/node.rb +373 -0
- data/lib/jsi/base.rb +738 -0
- data/lib/jsi/jsi_coder.rb +92 -0
- data/lib/jsi/metaschema.rb +6 -0
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
- data/lib/jsi/metaschema_node.rb +262 -0
- data/lib/jsi/ptr.rb +314 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +13 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +14 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +13 -0
- data/lib/jsi/schema/draft06.rb +13 -0
- data/lib/jsi/schema/draft07.rb +13 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +183 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +110 -0
- data/lib/jsi/schema/validation/draft06.rb +120 -0
- data/lib/jsi/schema/validation/draft07.rb +157 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +49 -0
- data/lib/jsi/schema.rb +792 -0
- data/lib/jsi/schema_classes.rb +357 -0
- data/lib/jsi/schema_registry.rb +190 -0
- data/lib/jsi/schema_set.rb +219 -0
- data/lib/jsi/simple_wrap.rb +26 -0
- data/lib/jsi/util/private/attr_struct.rb +130 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +202 -0
- data/lib/jsi/util/typelike.rb +225 -0
- data/lib/jsi/util.rb +227 -0
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +212 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +5 -0
- data/lib/jsi.rb +105 -0
- data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
- data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +155 -0
@@ -0,0 +1,357 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# A Module associated with a JSI Schema. See {Schema#jsi_schema_module}.
|
5
|
+
class SchemaModule < Module
|
6
|
+
# @private
|
7
|
+
def initialize(schema, &block)
|
8
|
+
super(&block)
|
9
|
+
|
10
|
+
@jsi_node = schema
|
11
|
+
|
12
|
+
schema.jsi_schemas.each do |schema_schema|
|
13
|
+
extend SchemaClasses.schema_property_reader_module(schema_schema, conflicting_modules: Set[SchemaModule])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# The schema for which this is the JSI Schema Module
|
18
|
+
# @return [Base + Schema]
|
19
|
+
def schema
|
20
|
+
@jsi_node
|
21
|
+
end
|
22
|
+
|
23
|
+
# a URI which refers to the schema. see {Schema#schema_uri}.
|
24
|
+
# @return (see Schema#schema_uri)
|
25
|
+
def schema_uri
|
26
|
+
schema.schema_uri
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
def inspect
|
31
|
+
if name_from_ancestor
|
32
|
+
if schema.schema_absolute_uri
|
33
|
+
-"#{name_from_ancestor} <#{schema.schema_absolute_uri}> (JSI Schema Module)"
|
34
|
+
else
|
35
|
+
-"#{name_from_ancestor} (JSI Schema Module)"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
-"(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :to_s, :inspect
|
43
|
+
|
44
|
+
# invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
|
45
|
+
#
|
46
|
+
# @param (see JSI::Schema#new_jsi)
|
47
|
+
# @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
|
48
|
+
# inplace applicators of this module's schema.
|
49
|
+
def new_jsi(instance, **kw)
|
50
|
+
schema.new_jsi(instance, **kw)
|
51
|
+
end
|
52
|
+
|
53
|
+
# See {Schema#schema_content}
|
54
|
+
def schema_content
|
55
|
+
schema.jsi_node_content
|
56
|
+
end
|
57
|
+
|
58
|
+
# See {Schema#instance_validate}
|
59
|
+
def instance_validate(instance)
|
60
|
+
schema.instance_validate(instance)
|
61
|
+
end
|
62
|
+
|
63
|
+
# See {Schema#instance_valid?}
|
64
|
+
def instance_valid?(instance)
|
65
|
+
schema.instance_valid?(instance)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# A module to extend the {SchemaModule} of a schema which describes other schemas (a {Schema::DescribesSchema})
|
70
|
+
module SchemaModule::DescribesSchemaModule
|
71
|
+
# Instantiates the given schema content as a JSI Schema.
|
72
|
+
#
|
73
|
+
# see {JSI::Schema::DescribesSchema#new_schema}
|
74
|
+
#
|
75
|
+
# @param (see JSI::Schema::DescribesSchema#new_schema)
|
76
|
+
# @yield (see JSI::Schema::DescribesSchema#new_schema)
|
77
|
+
# @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
|
78
|
+
# the given `schema_content` and whose schemas are inplace applicators of this module's schema
|
79
|
+
def new_schema(schema_content, **kw, &block)
|
80
|
+
schema.new_schema(schema_content, **kw, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
# (see Schema::DescribesSchema#new_schema_module)
|
84
|
+
def new_schema_module(schema_content, **kw, &block)
|
85
|
+
schema.new_schema(schema_content, **kw, &block).jsi_schema_module
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Set<Module>]
|
89
|
+
attr_reader :schema_implementation_modules
|
90
|
+
end
|
91
|
+
|
92
|
+
# this module is a namespace for building schema classes and schema modules.
|
93
|
+
module SchemaClasses
|
94
|
+
class << self
|
95
|
+
# @api private
|
96
|
+
# @return [Set<Module>]
|
97
|
+
def includes_for(instance)
|
98
|
+
includes = Set[]
|
99
|
+
includes << Base::ArrayNode if instance.respond_to?(:to_ary)
|
100
|
+
includes << Base::HashNode if instance.respond_to?(:to_hash)
|
101
|
+
includes << Base::StringNode if instance.respond_to?(:to_str)
|
102
|
+
includes.freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
# a JSI Schema Class which represents the given schemas.
|
106
|
+
# an instance of the class is a JSON Schema instance described by all of the given schemas.
|
107
|
+
# @api private
|
108
|
+
# @param schemas [Enumerable<JSI::Schema>] schemas which the class will represent
|
109
|
+
# @param includes [Enumerable<Module>] modules which will be included on the class
|
110
|
+
# @return [Class subclassing JSI::Base]
|
111
|
+
def class_for_schemas(schemas, includes: )
|
112
|
+
@class_for_schemas_map[
|
113
|
+
schemas: SchemaSet.ensure_schema_set(schemas),
|
114
|
+
includes: Util.ensure_module_set(includes),
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
private def class_for_schemas_compute(schemas: , includes: )
|
119
|
+
Class.new(Base) do
|
120
|
+
define_singleton_method(:jsi_class_schemas) { schemas }
|
121
|
+
define_method(:jsi_schemas) { schemas }
|
122
|
+
|
123
|
+
define_singleton_method(:jsi_class_includes) { includes }
|
124
|
+
|
125
|
+
conflicting_modules = Set[JSI::Base] + includes + schemas.map(&:jsi_schema_module)
|
126
|
+
|
127
|
+
reader_modules = schemas.map do |schema|
|
128
|
+
JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
|
129
|
+
end
|
130
|
+
reader_modules.each { |m| include m }
|
131
|
+
readers = reader_modules.map(&:jsi_property_readers).inject(Set[], &:merge).freeze
|
132
|
+
define_method(:jsi_property_readers) { readers }
|
133
|
+
define_singleton_method(:jsi_property_readers) { readers }
|
134
|
+
|
135
|
+
writer_modules = schemas.map do |schema|
|
136
|
+
JSI::SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules)
|
137
|
+
end
|
138
|
+
writer_modules.each { |m| include m }
|
139
|
+
|
140
|
+
includes.each { |m| include(m) }
|
141
|
+
schemas.each { |schema| include(schema.jsi_schema_module) }
|
142
|
+
jsi_class = self
|
143
|
+
define_method(:jsi_class) { jsi_class }
|
144
|
+
|
145
|
+
self
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# a subclass of MetaschemaNode::BootstrapSchema with the given modules included
|
150
|
+
# @api private
|
151
|
+
# @param modules [Set<Module>] schema implementation modules
|
152
|
+
# @return [Class]
|
153
|
+
def bootstrap_schema_class(modules)
|
154
|
+
@bootstrap_schema_class_map[
|
155
|
+
modules: Util.ensure_module_set(modules),
|
156
|
+
]
|
157
|
+
end
|
158
|
+
|
159
|
+
private def bootstrap_schema_class_compute(modules: )
|
160
|
+
Class.new(MetaschemaNode::BootstrapSchema) do
|
161
|
+
define_singleton_method(:schema_implementation_modules) { modules }
|
162
|
+
define_method(:schema_implementation_modules) { modules }
|
163
|
+
modules.each { |mod| include(mod) }
|
164
|
+
|
165
|
+
self
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# see {Schema#jsi_schema_module}
|
170
|
+
# @api private
|
171
|
+
# @return [Module + SchemaModule]
|
172
|
+
def module_for_schema(schema)
|
173
|
+
Schema.ensure_schema(schema)
|
174
|
+
raise(Bug, "non-Base schema cannot have schema module: #{schema}") unless schema.is_a?(Base)
|
175
|
+
@schema_module_map[schema: schema]
|
176
|
+
end
|
177
|
+
|
178
|
+
private def schema_module_compute(schema: )
|
179
|
+
SchemaModule.new(schema)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @deprecated after v0.7
|
183
|
+
def accessor_module_for_schema(schema, conflicting_modules: , setters: true)
|
184
|
+
Module.new do
|
185
|
+
include SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
|
186
|
+
include SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules) if setters
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# a module of readers for described property names of the given schema.
|
191
|
+
#
|
192
|
+
# @api private
|
193
|
+
# @param schema [JSI::Schema] a schema for which to define readers for any described property names
|
194
|
+
# @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
|
195
|
+
# may be used alongside the accessor module. methods defined by any conflicting_module
|
196
|
+
# will not be defined as accessors.
|
197
|
+
# @return [Module]
|
198
|
+
def schema_property_reader_module(schema, conflicting_modules: )
|
199
|
+
Schema.ensure_schema(schema)
|
200
|
+
@schema_property_reader_module_map[schema: schema, conflicting_modules: conflicting_modules]
|
201
|
+
end
|
202
|
+
|
203
|
+
private def schema_property_reader_module_compute(schema: , conflicting_modules: )
|
204
|
+
Module.new do
|
205
|
+
define_singleton_method(:inspect) { '(JSI Schema Property Reader Module)' }
|
206
|
+
|
207
|
+
readers = schema.described_object_property_names.select do |name|
|
208
|
+
Util.ok_ruby_method_name?(name) &&
|
209
|
+
!conflicting_modules.any? { |m| m.method_defined?(name) || m.private_method_defined?(name) }
|
210
|
+
end.to_set.freeze
|
211
|
+
|
212
|
+
define_singleton_method(:jsi_property_readers) { readers }
|
213
|
+
|
214
|
+
readers.each do |property_name|
|
215
|
+
define_method(property_name) do |**kw, &block|
|
216
|
+
self[property_name, **kw, &block]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# a module of writers for described property names of the given schema.
|
223
|
+
# @api private
|
224
|
+
def schema_property_writer_module(schema, conflicting_modules: )
|
225
|
+
Schema.ensure_schema(schema)
|
226
|
+
@schema_property_writer_module_map[schema: schema, conflicting_modules: conflicting_modules]
|
227
|
+
end
|
228
|
+
|
229
|
+
private def schema_property_writer_module_compute(schema: , conflicting_modules: )
|
230
|
+
Module.new do
|
231
|
+
define_singleton_method(:inspect) { '(JSI Schema Property Writer Module)' }
|
232
|
+
|
233
|
+
writers = schema.described_object_property_names.select do |name|
|
234
|
+
writer = "#{name}="
|
235
|
+
Util.ok_ruby_method_name?(name) &&
|
236
|
+
!conflicting_modules.any? { |m| m.method_defined?(writer) || m.private_method_defined?(writer) }
|
237
|
+
end.to_set.freeze
|
238
|
+
|
239
|
+
define_singleton_method(:jsi_property_writers) { writers }
|
240
|
+
|
241
|
+
writers.each do |property_name|
|
242
|
+
define_method("#{property_name}=") do |value|
|
243
|
+
self[property_name] = value
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
@class_for_schemas_map = Hash.new { |h, k| h[k] = class_for_schemas_compute(**k) }
|
251
|
+
@bootstrap_schema_class_map = Hash.new { |h, k| h[k] = bootstrap_schema_class_compute(**k) }
|
252
|
+
@schema_module_map = Hash.new { |h, k| h[k] = schema_module_compute(**k) }
|
253
|
+
@schema_property_reader_module_map = Hash.new { |h, k| h[k] = schema_property_reader_module_compute(**k) }
|
254
|
+
@schema_property_writer_module_map = Hash.new { |h, k| h[k] = schema_property_writer_module_compute(**k) }
|
255
|
+
end
|
256
|
+
|
257
|
+
# connecting {SchemaModule}s via {SchemaModule::Connection}s
|
258
|
+
module SchemaModule::Connects
|
259
|
+
attr_reader :jsi_node
|
260
|
+
|
261
|
+
# a name relative to a named schema module of an ancestor schema.
|
262
|
+
# for example, if `Foos = JSI::JSONSchemaDraft07.new_schema_module({'items' => {}})`
|
263
|
+
# then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
|
264
|
+
# @api private
|
265
|
+
# @return [String, nil]
|
266
|
+
def name_from_ancestor
|
267
|
+
named_ancestor_schema, tokens = named_ancestor_schema_tokens
|
268
|
+
return nil unless named_ancestor_schema
|
269
|
+
|
270
|
+
name = named_ancestor_schema.jsi_schema_module.name
|
271
|
+
ancestor = named_ancestor_schema
|
272
|
+
tokens.each do |token|
|
273
|
+
if ancestor.jsi_property_readers.include?(token)
|
274
|
+
name += ".#{token}"
|
275
|
+
elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
|
276
|
+
name += "[#{token.inspect}]"
|
277
|
+
else
|
278
|
+
return nil
|
279
|
+
end
|
280
|
+
ancestor = ancestor[token]
|
281
|
+
end
|
282
|
+
name
|
283
|
+
end
|
284
|
+
|
285
|
+
# Subscripting a JSI schema module or a {SchemaModule::Connection} will subscript its node, and
|
286
|
+
# if the result is a JSI::Schema, return the JSI Schema module of that schema; if it is a JSI::Base,
|
287
|
+
# return a SchemaModule::Connection; or if it is another value (a basic type), return that value.
|
288
|
+
#
|
289
|
+
# @param token [Object]
|
290
|
+
# @yield If the token identifies a schema and a block is given,
|
291
|
+
# it is evaluated in the context of the schema's JSI schema module
|
292
|
+
# using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
|
293
|
+
# @return [Module, SchemaModule::Connection, Object]
|
294
|
+
def [](token, **kw, &block)
|
295
|
+
raise(ArgumentError) unless kw.empty? # TODO remove eventually (keyword argument compatibility)
|
296
|
+
sub = @jsi_node[token]
|
297
|
+
if sub.is_a?(JSI::Schema)
|
298
|
+
sub.jsi_schema_module_exec(&block) if block
|
299
|
+
sub.jsi_schema_module
|
300
|
+
elsif block
|
301
|
+
raise(ArgumentError, "block given but token #{token.inspect} does not identify a schema")
|
302
|
+
elsif sub.is_a?(JSI::Base)
|
303
|
+
SchemaModule::Connection.new(sub)
|
304
|
+
else
|
305
|
+
sub
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
|
311
|
+
# @return [Array<JSI::Schema, Array>, nil]
|
312
|
+
def named_ancestor_schema_tokens
|
313
|
+
schema_ancestors = @jsi_node.jsi_ancestor_nodes
|
314
|
+
named_ancestor_schema = schema_ancestors.detect { |jsi| jsi.is_a?(JSI::Schema) && jsi.jsi_schema_module.name }
|
315
|
+
return nil unless named_ancestor_schema
|
316
|
+
tokens = @jsi_node.jsi_ptr.relative_to(named_ancestor_schema.jsi_ptr).tokens
|
317
|
+
[named_ancestor_schema, tokens]
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class SchemaModule
|
322
|
+
include Connects
|
323
|
+
end
|
324
|
+
|
325
|
+
# A JSI Schema Module is a module which represents a schema. A SchemaModule::Connection represents
|
326
|
+
# a node in a schema's document which is not a schema, such as the 'properties'
|
327
|
+
# object (which contains schemas but is not a schema).
|
328
|
+
#
|
329
|
+
# instances of this class act as a stand-in to allow users to subscript or call property accessors on
|
330
|
+
# schema modules to refer to their subschemas' schema modules.
|
331
|
+
#
|
332
|
+
# A SchemaModule::Connection has readers for property names described by the node's schemas.
|
333
|
+
class SchemaModule::Connection
|
334
|
+
include SchemaModule::Connects
|
335
|
+
|
336
|
+
# @param node [JSI::Base]
|
337
|
+
def initialize(node)
|
338
|
+
raise(Bug, "node must be JSI::Base: #{node.pretty_inspect.chomp}") unless node.is_a?(JSI::Base)
|
339
|
+
raise(Bug, "node must not be JSI::Schema: #{node.pretty_inspect.chomp}") if node.is_a?(JSI::Schema)
|
340
|
+
@jsi_node = node
|
341
|
+
node.jsi_schemas.each do |schema|
|
342
|
+
extend(JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: [SchemaModule::Connection]))
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# @return [String]
|
347
|
+
def inspect
|
348
|
+
if name_from_ancestor
|
349
|
+
-"#{name_from_ancestor} (#{self.class})"
|
350
|
+
else
|
351
|
+
-"(#{self.class}: #{@jsi_node.jsi_ptr.uri})"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
alias_method :to_s, :inspect
|
356
|
+
end
|
357
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
class SchemaRegistry
|
5
|
+
# an exception raised when an attempt is made to register a resource using a URI which is already
|
6
|
+
# registered with another resource
|
7
|
+
class Collision < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# an exception raised when an attempt is made to access (register or find) a resource of the
|
11
|
+
# registry using a URI which is not absolute (it is a relative URI or it contains a fragment)
|
12
|
+
class NonAbsoluteURI < StandardError
|
13
|
+
end
|
14
|
+
|
15
|
+
# an exception raised when a URI we are looking for has not been registered
|
16
|
+
class ResourceNotFound < StandardError
|
17
|
+
attr_accessor :uri
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@resources = {}
|
22
|
+
@autoload_uris = {}
|
23
|
+
@resources_mutex = Mutex.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# registers the given resource and/or schema resources it contains in the registry.
|
27
|
+
#
|
28
|
+
# each descendent node of the resource (including the resource itself) is registered if it is a schema
|
29
|
+
# that has an absolute URI (generally defined by the '$id' keyword).
|
30
|
+
#
|
31
|
+
# the given resource itself will be registered, whether or not it is a schema, if it is the root
|
32
|
+
# of its document and was instantiated with the option `uri` specified.
|
33
|
+
#
|
34
|
+
# @param resource [JSI::Base] a JSI containing resources to register
|
35
|
+
# @return [void]
|
36
|
+
def register(resource)
|
37
|
+
unless resource.is_a?(JSI::Base)
|
38
|
+
raise(ArgumentError, "resource must be a JSI::Base. got: #{resource.pretty_inspect.chomp}")
|
39
|
+
end
|
40
|
+
unless resource.is_a?(JSI::Schema) || resource.jsi_ptr.root?
|
41
|
+
# unsure, should this be allowed? the given JSI is not a "resource" as we define it, but
|
42
|
+
# if this check is removed it will just register any resources (schemas) below the given JSI.
|
43
|
+
raise(ArgumentError, "undefined behavior: registration of a JSI which is not a schema and is not at the root of a document")
|
44
|
+
end
|
45
|
+
|
46
|
+
# allow for registration of resources at the root of a document whether or not they are schemas.
|
47
|
+
# jsi_schema_base_uri at the root comes from the `uri` parameter to new_jsi / new_schema.
|
48
|
+
if resource.jsi_schema_base_uri && resource.jsi_ptr.root?
|
49
|
+
register_single(resource.jsi_schema_base_uri, resource)
|
50
|
+
end
|
51
|
+
|
52
|
+
resource.jsi_each_descendent_node do |node|
|
53
|
+
if node.is_a?(JSI::Schema) && node.schema_absolute_uri
|
54
|
+
register_single(node.schema_absolute_uri, node)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# takes a URI identifying a resource to be loaded by the given block
|
62
|
+
# when a reference to the URI is followed.
|
63
|
+
#
|
64
|
+
# for example:
|
65
|
+
#
|
66
|
+
# JSI.schema_registry.autoload_uri('http://example.com/schema.json') do
|
67
|
+
# JSI.new_schema({
|
68
|
+
# '$schema' => 'http://json-schema.org/draft-07/schema#',
|
69
|
+
# '$id' => 'http://example.com/schema.json',
|
70
|
+
# 'title' => 'my schema',
|
71
|
+
# })
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# the block would normally load JSON from the filesystem or similar.
|
75
|
+
#
|
76
|
+
# @param uri [Addressable::URI]
|
77
|
+
# @yieldreturn [JSI::Base] a JSI instance containing the resource identified by the given uri
|
78
|
+
# @return [void]
|
79
|
+
def autoload_uri(uri, &block)
|
80
|
+
uri = ensure_uri_absolute(uri)
|
81
|
+
mutating
|
82
|
+
unless block
|
83
|
+
raise(ArgumentError, ["#{SchemaRegistry}#autoload_uri must be invoked with a block", "URI: #{uri}"].join("\n"))
|
84
|
+
end
|
85
|
+
if @autoload_uris.key?(uri)
|
86
|
+
raise(Collision, ["already registered URI for autoload", "URI: #{uri}", "loader: #{@autoload_uris[uri]}"].join("\n"))
|
87
|
+
end
|
88
|
+
@autoload_uris[uri] = block
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param uri [Addressable::URI, #to_str]
|
93
|
+
# @return [JSI::Base]
|
94
|
+
# @raise [JSI::SchemaRegistry::ResourceNotFound]
|
95
|
+
def find(uri)
|
96
|
+
uri = ensure_uri_absolute(uri)
|
97
|
+
if @autoload_uris.key?(uri)
|
98
|
+
autoloaded = @autoload_uris[uri].call
|
99
|
+
register(autoloaded)
|
100
|
+
@autoload_uris.delete(uri)
|
101
|
+
end
|
102
|
+
if !@resources.key?(uri)
|
103
|
+
if autoloaded
|
104
|
+
msg = [
|
105
|
+
"URI #{uri} was registered with autoload_uri but the result did not contain a resource with that URI.",
|
106
|
+
"the resource resulting from autoload_uri was:",
|
107
|
+
autoloaded.pretty_inspect.chomp,
|
108
|
+
]
|
109
|
+
else
|
110
|
+
msg = ["URI #{uri} is not registered. registered URIs:", *(@resources.keys | @autoload_uris.keys)]
|
111
|
+
end
|
112
|
+
raise(ResourceNotFound.new(msg.join("\n")).tap { |e| e.uri = uri })
|
113
|
+
end
|
114
|
+
@resources[uri]
|
115
|
+
end
|
116
|
+
|
117
|
+
def inspect
|
118
|
+
[
|
119
|
+
'#<JSI::SchemaRegistry',
|
120
|
+
*[['autoload', @autoload_uris.keys], ['resources', @resources.keys]].map do |label, uris|
|
121
|
+
[
|
122
|
+
" #{label} (#{uris.size})#{uris.empty? ? "" : ":"}",
|
123
|
+
*uris.map do |uri|
|
124
|
+
" #{uri}"
|
125
|
+
end,
|
126
|
+
]
|
127
|
+
end.inject([], &:+),
|
128
|
+
'>',
|
129
|
+
].join("\n").freeze
|
130
|
+
end
|
131
|
+
|
132
|
+
alias_method :to_s, :inspect
|
133
|
+
|
134
|
+
def dup
|
135
|
+
self.class.new.tap do |reg|
|
136
|
+
@resources.each do |uri, resource|
|
137
|
+
reg.register_single(uri, resource)
|
138
|
+
end
|
139
|
+
@autoload_uris.each do |uri, autoload|
|
140
|
+
reg.autoload_uri(uri, &autoload)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def freeze
|
146
|
+
@resources.freeze
|
147
|
+
@autoload_uris.freeze
|
148
|
+
@resources_mutex = nil
|
149
|
+
super
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
# @param uri [Addressable::URI]
|
154
|
+
# @param resource [JSI::Base]
|
155
|
+
# @return [void]
|
156
|
+
def register_single(uri, resource)
|
157
|
+
mutating
|
158
|
+
@resources_mutex.synchronize do
|
159
|
+
ensure_uri_absolute(uri)
|
160
|
+
if @resources.key?(uri)
|
161
|
+
if @resources[uri] != resource
|
162
|
+
raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
|
163
|
+
end
|
164
|
+
else
|
165
|
+
@resources[uri] = resource
|
166
|
+
end
|
167
|
+
end
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def ensure_uri_absolute(uri)
|
174
|
+
uri = Util.uri(uri)
|
175
|
+
if uri.fragment
|
176
|
+
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
|
177
|
+
end
|
178
|
+
if uri.relative?
|
179
|
+
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
|
180
|
+
end
|
181
|
+
uri
|
182
|
+
end
|
183
|
+
|
184
|
+
def mutating
|
185
|
+
if frozen?
|
186
|
+
raise(FrozenError, "cannot modify frozen #{self.class}")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|