jsi-dev 0.0.0.pre.kramdown
Sign up to get free protection for your applications and to get access to all the features.
- 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
|