jsi-dev 0.0.0.pre.kramdown
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
data/lib/jsi/schema.rb
ADDED
@@ -0,0 +1,792 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# JSI::Schema is a module which extends {JSI::Base} instances which represent JSON schemas.
|
5
|
+
#
|
6
|
+
# This module is included on the {Schema#jsi_schema_module JSI Schema module} of any schema
|
7
|
+
# which describes other schemas, i.e. is a metaschema or other {Schema::DescribesSchema}.
|
8
|
+
# Therefore, any JSI instance described by a schema which is a {Schema::DescribesSchema} is
|
9
|
+
# a schema and is extended by this module.
|
10
|
+
#
|
11
|
+
# The content of an instance which is a JSI::Schema (referred to in this context as schema_content) is
|
12
|
+
# typically a Hash (JSON object) or a boolean.
|
13
|
+
module Schema
|
14
|
+
autoload :Application, 'jsi/schema/application'
|
15
|
+
autoload :Validation, 'jsi/schema/validation'
|
16
|
+
|
17
|
+
autoload :Issue, 'jsi/schema/issue'
|
18
|
+
autoload :Ref, 'jsi/schema/ref'
|
19
|
+
|
20
|
+
autoload :SchemaAncestorNode, 'jsi/schema/schema_ancestor_node'
|
21
|
+
|
22
|
+
autoload :Draft04, 'jsi/schema/draft04'
|
23
|
+
autoload :Draft06, 'jsi/schema/draft06'
|
24
|
+
autoload :Draft07, 'jsi/schema/draft07'
|
25
|
+
|
26
|
+
class Error < StandardError
|
27
|
+
end
|
28
|
+
|
29
|
+
# an exception raised when a thing is expected to be a JSI::Schema, but is not
|
30
|
+
class NotASchemaError < Error
|
31
|
+
end
|
32
|
+
|
33
|
+
# an exception raised when we are unable to resolve a schema reference
|
34
|
+
class ReferenceError < StandardError
|
35
|
+
end
|
36
|
+
|
37
|
+
# extends any schema which uses the keyword '$id' to identify its canonical URI
|
38
|
+
module BigMoneyId
|
39
|
+
# the contents of a $id keyword whose value is a string, or nil
|
40
|
+
# @return [#to_str, nil]
|
41
|
+
def id
|
42
|
+
if keyword?('$id') && schema_content['$id'].respond_to?(:to_str)
|
43
|
+
schema_content['$id']
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# extends any schema which uses the keyword 'id' to identify its canonical URI
|
51
|
+
module OldId
|
52
|
+
# the contents of an `id` keyword whose value is a string, or nil
|
53
|
+
# @return [#to_str, nil]
|
54
|
+
def id
|
55
|
+
if keyword?('id') && schema_content['id'].respond_to?(:to_str)
|
56
|
+
schema_content['id']
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# extends any schema which defines an anchor as a URI fragment in the schema id
|
64
|
+
module IdWithAnchor
|
65
|
+
# a URI for the schema's id, unless the id defines an anchor in its
|
66
|
+
# fragment. nil if the schema defines no id.
|
67
|
+
# @return [Addressable::URI, nil]
|
68
|
+
def id_without_fragment
|
69
|
+
if id
|
70
|
+
id_uri = Util.uri(id)
|
71
|
+
if id_uri.merge(fragment: nil).empty?
|
72
|
+
# fragment-only id is just an anchor
|
73
|
+
# e.g. #foo
|
74
|
+
nil
|
75
|
+
elsif id_uri.fragment == nil
|
76
|
+
# no fragment
|
77
|
+
# e.g. http://localhost:1234/bar
|
78
|
+
id_uri
|
79
|
+
elsif id_uri.fragment == ''
|
80
|
+
# empty fragment
|
81
|
+
# e.g. http://json-schema.org/draft-07/schema#
|
82
|
+
id_uri.merge(fragment: nil).freeze
|
83
|
+
elsif jsi_schema_base_uri && jsi_schema_base_uri.join(id_uri).merge(fragment: nil) == jsi_schema_base_uri
|
84
|
+
# the id, resolved against the base uri, consists of the base uri plus an anchor fragment.
|
85
|
+
# so there's no non-fragment id.
|
86
|
+
# e.g. base uri is http://localhost:1234/bar
|
87
|
+
# and id is http://localhost:1234/bar#foo
|
88
|
+
nil
|
89
|
+
else
|
90
|
+
# e.g. http://localhost:1234/bar#foo
|
91
|
+
id_uri.merge(fragment: nil).freeze
|
92
|
+
end
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# an anchor defined by a non-empty fragment in the id uri
|
99
|
+
# @return [String]
|
100
|
+
def anchor
|
101
|
+
if id
|
102
|
+
id_uri = Util.uri(id)
|
103
|
+
if id_uri.fragment == ''
|
104
|
+
nil
|
105
|
+
else
|
106
|
+
id_uri.fragment
|
107
|
+
end
|
108
|
+
else
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
module IntegerAllows0Fraction
|
116
|
+
# is `value` an integer?
|
117
|
+
# @private
|
118
|
+
# @param value
|
119
|
+
# @return [Boolean]
|
120
|
+
def internal_integer?(value)
|
121
|
+
value.is_a?(Integer) || (value.is_a?(Numeric) && value % 1.0 == 0.0)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @private
|
126
|
+
module IntegerDisallows0Fraction
|
127
|
+
# is `value` an integer?
|
128
|
+
# @private
|
129
|
+
# @param value
|
130
|
+
# @return [Boolean]
|
131
|
+
def internal_integer?(value)
|
132
|
+
value.is_a?(Integer)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# This module extends any JSI Schema which describes schemas.
|
137
|
+
#
|
138
|
+
# Examples of a schema which describes schemas include the JSON Schema metaschemas and
|
139
|
+
# the OpenAPI schema definition which describes "A deterministic version of a JSON Schema object."
|
140
|
+
#
|
141
|
+
# Schemas which describes schemas include {JSI::Schema} in their
|
142
|
+
# {Schema#jsi_schema_module JSI Schema module}, so for a schema which is an instance of
|
143
|
+
# DescribesSchema, instances of that schema are instances of {JSI::Schema} and are schemas.
|
144
|
+
#
|
145
|
+
# A schema is indicated as describing other schemas using the {Schema#describes_schema!} method.
|
146
|
+
module DescribesSchema
|
147
|
+
# Instantiates the given schema content as a JSI Schema.
|
148
|
+
#
|
149
|
+
# By default, the schema will be registered with the {JSI.schema_registry}.
|
150
|
+
# This can be controlled by params `register` and `schema_registry`.
|
151
|
+
#
|
152
|
+
# By default, the `schema_content` will have any Symbol keys of Hashes replaced with Strings
|
153
|
+
# (recursively through the document). This is controlled by the param `stringify_symbol_keys`.
|
154
|
+
#
|
155
|
+
# @param schema_content an object to be instantiated as a JSI Schema - typically a Hash
|
156
|
+
# @param uri [#to_str, Addressable::URI] The retrieval URI of the schema document.
|
157
|
+
# If specified, the root schema will be identified by this URI, in addition
|
158
|
+
# to any absolute URI declared with an id keyword, for resolution in the `schema_registry`.
|
159
|
+
#
|
160
|
+
# It is rare that this needs to be specified. Most schemas, if they use absolute URIs, will
|
161
|
+
# use the `$id` keyword (`id` in draft 4) to specify this. A different retrieval URI is useful
|
162
|
+
# in unusual cases:
|
163
|
+
#
|
164
|
+
# - A schema in the document uses relative URIs for `$id` or `$ref` without an absolute id in an
|
165
|
+
# ancestor schema - these will be resolved relative to this URI
|
166
|
+
# - Another schema refers with `$ref` to the schema being instantiated by this retrieval URI,
|
167
|
+
# rather than an id declared in the schema - the schema is resolvable by this URI in the
|
168
|
+
# `schema_registry`.
|
169
|
+
# @param register [Boolean] Whether the instantiated schema and any subschemas with absolute URIs
|
170
|
+
# will be registered in the schema registry indicated by param `schema_registry`.
|
171
|
+
# @param schema_registry [SchemaRegistry, nil] The registry this schema will use.
|
172
|
+
#
|
173
|
+
# - The schema and subschemas will be registered here with any declared URI,
|
174
|
+
# unless the `register` param is false.
|
175
|
+
# - References from within the schema (typically from `$ref` keywords) are resolved using this registry.
|
176
|
+
# @param stringify_symbol_keys [Boolean] Whether the schema content will have any Symbol keys of Hashes
|
177
|
+
# replaced with Strings (recursively through the document).
|
178
|
+
# Replacement is done on a copy; the given schema content is not modified.
|
179
|
+
# @yield If a block is given, it is evaluated in the context of the schema's JSI schema module
|
180
|
+
# using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
|
181
|
+
# @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
|
182
|
+
# the given `schema_content` and whose schemas are this schema's inplace applicators.
|
183
|
+
def new_schema(schema_content,
|
184
|
+
uri: nil,
|
185
|
+
register: true,
|
186
|
+
schema_registry: JSI.schema_registry,
|
187
|
+
stringify_symbol_keys: true,
|
188
|
+
&block
|
189
|
+
)
|
190
|
+
schema_jsi = new_jsi(schema_content,
|
191
|
+
uri: uri,
|
192
|
+
register: register,
|
193
|
+
schema_registry: schema_registry,
|
194
|
+
stringify_symbol_keys: stringify_symbol_keys,
|
195
|
+
)
|
196
|
+
if block
|
197
|
+
schema_jsi.jsi_schema_module_exec(&block)
|
198
|
+
end
|
199
|
+
schema_jsi
|
200
|
+
end
|
201
|
+
|
202
|
+
# Instantiates the given schema content as a JSI Schema, passing all params to
|
203
|
+
# {Schema::DescribesSchema#new_schema}, and returns its {Schema#jsi_schema_module JSI Schema Module}.
|
204
|
+
#
|
205
|
+
# @return [Module + JSI::SchemaModule] the JSI Schema Module of the instantiated schema
|
206
|
+
def new_schema_module(schema_content, **kw, &block)
|
207
|
+
new_schema(schema_content, **kw, &block).jsi_schema_module
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class << self
|
212
|
+
def extended(o)
|
213
|
+
super
|
214
|
+
o.send(:jsi_schema_initialize)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class << self
|
220
|
+
# An application-wide default metaschema set by {default_metaschema=}, used by {JSI.new_schema}
|
221
|
+
# to instantiate schemas which do not specify their metaschema using a `$schema` property.
|
222
|
+
#
|
223
|
+
# @return [nil, Base + Schema + Schema::DescribesSchema]
|
224
|
+
def default_metaschema
|
225
|
+
@default_metaschema
|
226
|
+
end
|
227
|
+
|
228
|
+
# Sets {default_metaschema} to a schema indicated by the given param.
|
229
|
+
#
|
230
|
+
# @param default_metaschema [Schema::DescribesSchema, SchemaModule::DescribesSchemaModule, #to_str, nil]
|
231
|
+
# Indicates the default metaschema.
|
232
|
+
# This may be a metaschema or a metaschema's schema module (e.g. `JSI::JSONSchemaDraft07`),
|
233
|
+
# or a URI (as would be in a `$schema` keyword).
|
234
|
+
#
|
235
|
+
# `nil` to unset.
|
236
|
+
def default_metaschema=(default_metaschema)
|
237
|
+
@default_metaschema = default_metaschema.nil? ? nil : ensure_describes_schema(default_metaschema)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Instantiates the given schema content as a JSI Schema.
|
241
|
+
#
|
242
|
+
# The metaschema which describes the schema must be indicated:
|
243
|
+
#
|
244
|
+
# - If the schema object has a `$schema` property, that URI is resolved using the `schema_registry`
|
245
|
+
# param (by default {JSI.schema_registry}), and that metaschema is used. For example:
|
246
|
+
#
|
247
|
+
# ```ruby
|
248
|
+
# JSI.new_schema({
|
249
|
+
# "$schema" => "http://json-schema.org/draft-07/schema#",
|
250
|
+
# "properties" => ...,
|
251
|
+
# })
|
252
|
+
# ```
|
253
|
+
#
|
254
|
+
# - if no `$schema` property is present, the `default_metaschema` param is used, if the caller
|
255
|
+
# specifies it. For example:
|
256
|
+
#
|
257
|
+
# ```ruby
|
258
|
+
# JSI.new_schema({"properties" => ...}, default_metaschema: JSI::JSONSchemaDraft07)
|
259
|
+
# ```
|
260
|
+
#
|
261
|
+
# - if no `default_metaschema` param is specified, the application-wide default
|
262
|
+
# {JSI.default_metaschema JSI.default_metaschema} is used,
|
263
|
+
# if the application has set it. For example:
|
264
|
+
#
|
265
|
+
# ```ruby
|
266
|
+
# JSI.default_metaschema = JSI::JSONSchemaDraft07
|
267
|
+
# JSI.new_schema({"properties" => ...})
|
268
|
+
# ```
|
269
|
+
#
|
270
|
+
# an ArgumentError is raised if none of these indicate a metaschema to use.
|
271
|
+
#
|
272
|
+
# Note that if you are instantiating a schema known to have no `$schema` property, an alternative to
|
273
|
+
# specifying a `default_metaschema` is to call `new_schema` on the
|
274
|
+
# {Schema::DescribesSchema#new_schema metaschema} or its
|
275
|
+
# {SchemaModule::DescribesSchemaModule#new_schema schema module}, e.g.
|
276
|
+
# `JSI::JSONSchemaDraft07.new_schema(my_schema_content)`
|
277
|
+
#
|
278
|
+
# @param schema_content (see Schema::DescribesSchema#new_schema)
|
279
|
+
# @param default_metaschema [Schema::DescribesSchema, SchemaModule::DescribesSchemaModule, #to_str]
|
280
|
+
# Indicates the metaschema to use if the given schema_content does not have a `$schema` property.
|
281
|
+
# This may be a metaschema or a metaschema's schema module (e.g. `JSI::JSONSchemaDraft07`),
|
282
|
+
# or a URI (as would be in a `$schema` keyword).
|
283
|
+
# @param uri (see Schema::DescribesSchema#new_schema)
|
284
|
+
# @param register (see DescribesSchema#new_schema)
|
285
|
+
# @param schema_registry (see DescribesSchema#new_schema)
|
286
|
+
# @param stringify_symbol_keys (see Schema::DescribesSchema#new_schema)
|
287
|
+
# @yield (see Schema::DescribesSchema#new_schema)
|
288
|
+
# @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
|
289
|
+
# the given `schema_content` and whose schemas are inplace applicators of the indicated metaschema
|
290
|
+
def new_schema(schema_content,
|
291
|
+
default_metaschema: nil,
|
292
|
+
# params of DescribesSchema#new_schema have their default values repeated here. delegating in a splat
|
293
|
+
# would remove repetition, but yard doesn't display delegated defaults with its (see X) directive.
|
294
|
+
uri: nil,
|
295
|
+
register: true,
|
296
|
+
schema_registry: JSI.schema_registry,
|
297
|
+
stringify_symbol_keys: true,
|
298
|
+
&block
|
299
|
+
)
|
300
|
+
new_schema_params = {
|
301
|
+
uri: uri,
|
302
|
+
register: register,
|
303
|
+
schema_registry: schema_registry,
|
304
|
+
stringify_symbol_keys: stringify_symbol_keys,
|
305
|
+
}
|
306
|
+
default_metaschema_new_schema = -> {
|
307
|
+
default_metaschema = if default_metaschema
|
308
|
+
Schema.ensure_describes_schema(default_metaschema, name: "default_metaschema")
|
309
|
+
elsif self.default_metaschema
|
310
|
+
self.default_metaschema
|
311
|
+
else
|
312
|
+
raise(ArgumentError, [
|
313
|
+
"When instantiating a schema with no `$schema` property, you must specify its metaschema by one of these methods:",
|
314
|
+
"- pass the `default_metaschema` param to this method",
|
315
|
+
" e.g.: JSI.new_schema(..., default_metaschema: JSI::JSONSchemaDraft07)",
|
316
|
+
"- invoke `new_schema` on the appropriate metaschema or its schema module",
|
317
|
+
" e.g.: JSI::JSONSchemaDraft07.new_schema(...)",
|
318
|
+
"- set JSI.default_metaschema to an application-wide default metaschema initially",
|
319
|
+
" e.g.: JSI.default_metaschema = JSI::JSONSchemaDraft07",
|
320
|
+
"instantiating schema_content: #{schema_content.pretty_inspect.chomp}",
|
321
|
+
].join("\n"))
|
322
|
+
end
|
323
|
+
default_metaschema.new_schema(schema_content, **new_schema_params, &block)
|
324
|
+
}
|
325
|
+
if schema_content.is_a?(Schema)
|
326
|
+
raise(TypeError, [
|
327
|
+
"Given schema_content is already a JSI::Schema. It cannot be instantiated as the content of a schema.",
|
328
|
+
"given: #{schema_content.pretty_inspect.chomp}",
|
329
|
+
].join("\n"))
|
330
|
+
elsif schema_content.is_a?(JSI::Base)
|
331
|
+
raise(TypeError, [
|
332
|
+
"Given schema_content is a JSI::Base. It cannot be instantiated as the content of a schema.",
|
333
|
+
"given: #{schema_content.pretty_inspect.chomp}",
|
334
|
+
].join("\n"))
|
335
|
+
elsif schema_content.respond_to?(:to_hash)
|
336
|
+
id = schema_content['$schema'] || stringify_symbol_keys && schema_content[:'$schema']
|
337
|
+
if id
|
338
|
+
unless id.respond_to?(:to_str)
|
339
|
+
raise(ArgumentError, "given schema_content keyword `$schema` is not a string")
|
340
|
+
end
|
341
|
+
metaschema = Schema.ensure_describes_schema(id, name: '$schema', schema_registry: schema_registry)
|
342
|
+
metaschema.new_schema(schema_content, **new_schema_params, &block)
|
343
|
+
else
|
344
|
+
default_metaschema_new_schema.call
|
345
|
+
end
|
346
|
+
else
|
347
|
+
default_metaschema_new_schema.call
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
self.default_metaschema = nil
|
353
|
+
|
354
|
+
module Schema
|
355
|
+
class << self
|
356
|
+
# ensure the given object is a JSI Schema
|
357
|
+
#
|
358
|
+
# @param schema [Object] the thing the caller wishes to ensure is a Schema
|
359
|
+
# @param msg [#to_s, #to_ary] lines of the error message preceding the pretty-printed schema param
|
360
|
+
# if the schema param is not a schema
|
361
|
+
# @raise [NotASchemaError] if the schema param is not a schema
|
362
|
+
# @return [Schema] the given schema
|
363
|
+
def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
|
364
|
+
if schema.is_a?(Schema)
|
365
|
+
schema
|
366
|
+
else
|
367
|
+
if reinstantiate_as && schema.is_a?(JSI::Base)
|
368
|
+
# TODO warn; behavior is undefined and I hate this implementation
|
369
|
+
|
370
|
+
result_schema_schemas = schema.jsi_schemas + reinstantiate_as
|
371
|
+
|
372
|
+
result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_schemas,
|
373
|
+
includes: SchemaClasses.includes_for(schema.jsi_node_content)
|
374
|
+
)
|
375
|
+
|
376
|
+
result_schema_class.new(schema.jsi_document,
|
377
|
+
jsi_ptr: schema.jsi_ptr,
|
378
|
+
jsi_indicated_schemas: schema.jsi_indicated_schemas,
|
379
|
+
jsi_schema_base_uri: schema.jsi_schema_base_uri,
|
380
|
+
jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
|
381
|
+
jsi_schema_registry: schema.jsi_schema_registry,
|
382
|
+
jsi_root_node: schema.jsi_ptr.root? ? nil : schema.jsi_root_node, # bad
|
383
|
+
)
|
384
|
+
else
|
385
|
+
raise(NotASchemaError, [
|
386
|
+
*msg,
|
387
|
+
schema.pretty_inspect.chomp,
|
388
|
+
].join("\n"))
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Ensures the given param identifies a JSI Schema which describes schemas, and returns that schema.
|
394
|
+
#
|
395
|
+
# @api private
|
396
|
+
# @param describes_schema [Schema::DescribesSchema, SchemaModule::DescribesSchemaModule, #to_str]
|
397
|
+
# @raise [TypeError] if the param does not indicate a schema which describes schemas
|
398
|
+
# @return [Base + Schema + Schema::DescribesSchema]
|
399
|
+
def ensure_describes_schema(describes_schema, name: nil, schema_registry: JSI.schema_registry)
|
400
|
+
if describes_schema.respond_to?(:to_str)
|
401
|
+
schema = Schema::Ref.new(describes_schema, schema_registry: schema_registry).deref_schema
|
402
|
+
if !schema.describes_schema?
|
403
|
+
raise(TypeError, [name, "URI indicates a schema which does not describe schemas: #{describes_schema.pretty_inspect.chomp}"].compact.join(" "))
|
404
|
+
end
|
405
|
+
schema
|
406
|
+
elsif describes_schema.is_a?(SchemaModule::DescribesSchemaModule)
|
407
|
+
describes_schema.schema
|
408
|
+
elsif describes_schema.is_a?(DescribesSchema)
|
409
|
+
describes_schema
|
410
|
+
else
|
411
|
+
raise(TypeError, "#{name || "param"} does not indicate a schema which describes schemas: #{describes_schema.pretty_inspect.chomp}")
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
417
|
+
def initialize(*)
|
418
|
+
super
|
419
|
+
jsi_schema_initialize
|
420
|
+
end
|
421
|
+
else
|
422
|
+
def initialize(*, **)
|
423
|
+
super
|
424
|
+
jsi_schema_initialize
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# the underlying JSON data used to instantiate this JSI::Schema.
|
429
|
+
# this is an alias for {Base#jsi_node_content}, named for clarity in the context of working with
|
430
|
+
# a schema.
|
431
|
+
def schema_content
|
432
|
+
jsi_node_content
|
433
|
+
end
|
434
|
+
|
435
|
+
# does this schema contain the given keyword?
|
436
|
+
# @return [Boolean]
|
437
|
+
def keyword?(keyword)
|
438
|
+
schema_content = jsi_node_content
|
439
|
+
schema_content.respond_to?(:to_hash) && schema_content.key?(keyword)
|
440
|
+
end
|
441
|
+
|
442
|
+
# the URI of this schema, calculated from our `#id`, resolved against our `#jsi_schema_base_uri`
|
443
|
+
# @return [Addressable::URI, nil]
|
444
|
+
def schema_absolute_uri
|
445
|
+
if respond_to?(:id_without_fragment) && id_without_fragment
|
446
|
+
if jsi_schema_base_uri
|
447
|
+
jsi_schema_base_uri.join(id_without_fragment).freeze
|
448
|
+
elsif id_without_fragment.absolute?
|
449
|
+
id_without_fragment
|
450
|
+
else
|
451
|
+
# TODO warn / schema_error
|
452
|
+
nil
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# a nonrelative URI which refers to this schema.
|
458
|
+
# nil if no parent of this schema defines an id.
|
459
|
+
# see {#schema_uris} for all URIs known to refer to this schema.
|
460
|
+
# @return [Addressable::URI, nil]
|
461
|
+
def schema_uri
|
462
|
+
schema_uris.first
|
463
|
+
end
|
464
|
+
|
465
|
+
# nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema
|
466
|
+
# @return [Array<Addressable::URI>]
|
467
|
+
def schema_uris
|
468
|
+
@schema_uris_map[]
|
469
|
+
end
|
470
|
+
|
471
|
+
private def schema_uris_compute(**_) # TODO remove **_ eventually (keyword argument compatibility)
|
472
|
+
each_schema_uri.to_a
|
473
|
+
end
|
474
|
+
|
475
|
+
# see {#schema_uris}
|
476
|
+
# @yield [Addressable::URI]
|
477
|
+
# @return [Enumerator, nil]
|
478
|
+
def each_schema_uri
|
479
|
+
return to_enum(__method__) unless block_given?
|
480
|
+
|
481
|
+
yield schema_absolute_uri if schema_absolute_uri
|
482
|
+
|
483
|
+
parent_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
|
484
|
+
resource.schema_absolute_uri
|
485
|
+
end
|
486
|
+
|
487
|
+
anchored = respond_to?(:anchor) ? anchor : nil
|
488
|
+
parent_schemas.each do |parent_schema|
|
489
|
+
if anchored
|
490
|
+
if parent_schema.jsi_anchor_subschema(anchor) == self
|
491
|
+
yield parent_schema.schema_absolute_uri.merge(fragment: anchor).freeze
|
492
|
+
else
|
493
|
+
anchored = false
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
relative_ptr = jsi_ptr.relative_to(parent_schema.jsi_ptr)
|
498
|
+
yield parent_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment).freeze
|
499
|
+
end
|
500
|
+
|
501
|
+
nil
|
502
|
+
end
|
503
|
+
|
504
|
+
# a module which extends all instances of this schema. this may be opened by the application to add
|
505
|
+
# methods to schema instances.
|
506
|
+
#
|
507
|
+
# some functionality is also defined on the module itself (its singleton class, not for its instances):
|
508
|
+
#
|
509
|
+
# - the module is extended with {JSI::SchemaModule}, which defines .new_jsi to instantiate instances
|
510
|
+
# of this schema (see {#new_jsi}).
|
511
|
+
# - properties described by this schema's metaschema are defined as methods to get subschemas' schema
|
512
|
+
# modules, so for example `schema.jsi_schema_module.items` returns the same module
|
513
|
+
# as `schema.items.jsi_schema_module`.
|
514
|
+
# - method .schema which returns this schema.
|
515
|
+
#
|
516
|
+
# @return [Module + SchemaModule]
|
517
|
+
def jsi_schema_module
|
518
|
+
JSI::SchemaClasses.module_for_schema(self)
|
519
|
+
end
|
520
|
+
|
521
|
+
# Evaluates the given block in the context of this schema's JSI schema module.
|
522
|
+
# Any arguments passed to this method will be passed to the block.
|
523
|
+
# shortcut to invoke [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec)
|
524
|
+
# on our {#jsi_schema_module}.
|
525
|
+
#
|
526
|
+
# @return the result of evaluating the block
|
527
|
+
def jsi_schema_module_exec(*a, **kw, &block)
|
528
|
+
jsi_schema_module.module_exec(*a, **kw, &block)
|
529
|
+
end
|
530
|
+
|
531
|
+
# Instantiates a new JSI whose content comes from the given `instance` param.
|
532
|
+
# This schema indicates the schemas of the JSI - its schemas are inplace
|
533
|
+
# applicators of this schema which apply to the given instance.
|
534
|
+
#
|
535
|
+
# @param (see SchemaSet#new_jsi)
|
536
|
+
# @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
|
537
|
+
# inplace applicators of this schema.
|
538
|
+
def new_jsi(instance, **kw)
|
539
|
+
SchemaSet[self].new_jsi(instance, **kw)
|
540
|
+
end
|
541
|
+
|
542
|
+
# @param keyword schema keyword e.g. "$ref", "$schema"
|
543
|
+
# @return [Schema::Ref]
|
544
|
+
def schema_ref(keyword: "$ref")
|
545
|
+
raise(ArgumentError, "keyword not present: #{keyword}") unless keyword?(keyword)
|
546
|
+
@schema_ref_map[keyword: keyword, value: schema_content[keyword]]
|
547
|
+
end
|
548
|
+
|
549
|
+
# does this schema itself describe a schema?
|
550
|
+
# @return [Boolean]
|
551
|
+
def describes_schema?
|
552
|
+
jsi_schema_module <= JSI::Schema || false
|
553
|
+
end
|
554
|
+
|
555
|
+
# indicates that this schema describes a schema.
|
556
|
+
# this schema is extended with {DescribesSchema} and its {#jsi_schema_module} is extended
|
557
|
+
# with {SchemaModule::DescribesSchemaModule}, and the JSI Schema Module will include
|
558
|
+
# JSI::Schema and the given modules.
|
559
|
+
#
|
560
|
+
# @param schema_implementation_modules [Enumerable<Module>] modules which implement the functionality of
|
561
|
+
# the schema to extend schemas described by this schema.
|
562
|
+
# @return [void]
|
563
|
+
def describes_schema!(schema_implementation_modules)
|
564
|
+
schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)
|
565
|
+
|
566
|
+
if describes_schema?
|
567
|
+
# this schema, or one equal to it, has already had describes_schema! called on it.
|
568
|
+
# this is to be avoided, but is not particularly a problem.
|
569
|
+
# it is a bug if it was called different times with different schema_implementation_modules, though.
|
570
|
+
unless jsi_schema_module.schema_implementation_modules == schema_implementation_modules
|
571
|
+
raise(ArgumentError, "this schema already describes a schema with different schema_implementation_modules")
|
572
|
+
end
|
573
|
+
else
|
574
|
+
jsi_schema_module.include(Schema)
|
575
|
+
schema_implementation_modules.each do |mod|
|
576
|
+
jsi_schema_module.include(mod)
|
577
|
+
end
|
578
|
+
jsi_schema_module.extend(SchemaModule::DescribesSchemaModule)
|
579
|
+
jsi_schema_module.instance_variable_set(:@schema_implementation_modules, schema_implementation_modules)
|
580
|
+
end
|
581
|
+
|
582
|
+
extend(DescribesSchema)
|
583
|
+
|
584
|
+
nil
|
585
|
+
end
|
586
|
+
|
587
|
+
# a resource containing this schema.
|
588
|
+
#
|
589
|
+
# if any parent, or this schema itself, is a schema with an absolute uri (see {#schema_absolute_uri}),
|
590
|
+
# the resource root is the closest schema with an absolute uri.
|
591
|
+
#
|
592
|
+
# If no parent schema has an absolute uri, the schema_resource_root is the {Base#jsi_root_node document's root node}.
|
593
|
+
# In this case, the resource root may or may not be a schema itself.
|
594
|
+
#
|
595
|
+
# @return [JSI::Base] resource containing this schema
|
596
|
+
def schema_resource_root
|
597
|
+
jsi_subschema_resource_ancestors.last || jsi_root_node
|
598
|
+
end
|
599
|
+
|
600
|
+
# is this schema the root of a schema resource?
|
601
|
+
# @return [Boolean]
|
602
|
+
def schema_resource_root?
|
603
|
+
jsi_ptr.root? || !!schema_absolute_uri
|
604
|
+
end
|
605
|
+
|
606
|
+
# a subschema of this Schema
|
607
|
+
#
|
608
|
+
# @param subptr [JSI::Ptr, #to_ary] a relative pointer, or array of tokens, pointing to the subschema
|
609
|
+
# @return [JSI::Schema] the subschema at the location indicated by subptr. self if subptr is empty.
|
610
|
+
def subschema(subptr)
|
611
|
+
@subschema_map[subptr: Ptr.ary_ptr(subptr)]
|
612
|
+
end
|
613
|
+
|
614
|
+
private def subschema_compute(subptr: )
|
615
|
+
Schema.ensure_schema(jsi_descendent_node(subptr), msg: [
|
616
|
+
"subschema is not a schema at pointer: #{subptr.pointer}"
|
617
|
+
])
|
618
|
+
end
|
619
|
+
|
620
|
+
# a schema in the same schema resource as this one (see {#schema_resource_root}) at the given
|
621
|
+
# pointer relative to the root of the schema resource.
|
622
|
+
#
|
623
|
+
# @param ptr [JSI::Ptr, #to_ary] a pointer to a schema from our schema resource root
|
624
|
+
# @return [JSI::Schema] the schema pointed to by ptr
|
625
|
+
def resource_root_subschema(ptr)
|
626
|
+
@resource_root_subschema_map[ptr: Ptr.ary_ptr(ptr)]
|
627
|
+
end
|
628
|
+
|
629
|
+
private def resource_root_subschema_compute(ptr: )
|
630
|
+
Schema.ensure_schema(schema_resource_root.jsi_descendent_node(ptr),
|
631
|
+
msg: [
|
632
|
+
"subschema is not a schema at pointer: #{ptr.pointer}"
|
633
|
+
],
|
634
|
+
reinstantiate_as: jsi_schemas.select(&:describes_schema?)
|
635
|
+
)
|
636
|
+
end
|
637
|
+
|
638
|
+
# a set of inplace applicator schemas of this schema (from $ref, allOf, etc.) which apply to the
|
639
|
+
# given instance.
|
640
|
+
#
|
641
|
+
# the returned set will contain this schema itself, unless this schema contains a $ref keyword.
|
642
|
+
#
|
643
|
+
# @param instance [Object] the instance to check any applicators against
|
644
|
+
# @return [JSI::SchemaSet] matched applicator schemas
|
645
|
+
def inplace_applicator_schemas(instance)
|
646
|
+
SchemaSet.new(each_inplace_applicator_schema(instance))
|
647
|
+
end
|
648
|
+
|
649
|
+
# yields each inplace applicator schema which applies to the given instance.
|
650
|
+
#
|
651
|
+
# @param instance (see #inplace_applicator_schemas)
|
652
|
+
# @param visited_refs [Enumerable<JSI::Schema::Ref>]
|
653
|
+
# @yield [JSI::Schema]
|
654
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
655
|
+
def each_inplace_applicator_schema(instance, visited_refs: [], &block)
|
656
|
+
return to_enum(__method__, instance, visited_refs: visited_refs) unless block
|
657
|
+
|
658
|
+
catch(:jsi_application_done) do
|
659
|
+
internal_inplace_applicate_keywords(instance, visited_refs, &block)
|
660
|
+
end
|
661
|
+
|
662
|
+
nil
|
663
|
+
end
|
664
|
+
|
665
|
+
# a set of child applicator subschemas of this schema which apply to the child of the given instance
|
666
|
+
# on the given token.
|
667
|
+
#
|
668
|
+
# @param token [Object] the array index or object property name for the child instance
|
669
|
+
# @param instance [Object] the instance to check any child applicators against
|
670
|
+
# @return [JSI::SchemaSet] child applicator subschemas of this schema for the given token
|
671
|
+
# of the instance
|
672
|
+
def child_applicator_schemas(token, instance)
|
673
|
+
SchemaSet.new(each_child_applicator_schema(token, instance))
|
674
|
+
end
|
675
|
+
|
676
|
+
# yields each child applicator subschema (from properties, items, etc.) which applies to the child of
|
677
|
+
# the given instance on the given token.
|
678
|
+
#
|
679
|
+
# @param (see #child_applicator_schemas)
|
680
|
+
# @yield [JSI::Schema]
|
681
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
682
|
+
def each_child_applicator_schema(token, instance, &block)
|
683
|
+
return to_enum(__method__, token, instance) unless block
|
684
|
+
|
685
|
+
internal_child_applicate_keywords(token, instance, &block)
|
686
|
+
|
687
|
+
nil
|
688
|
+
end
|
689
|
+
|
690
|
+
# any object property names this schema indicates may be present on its instances.
|
691
|
+
# this includes any keys of this schema's "properties" object and any entries of this schema's
|
692
|
+
# array of "required" property keys.
|
693
|
+
# @return [Set]
|
694
|
+
def described_object_property_names
|
695
|
+
@described_object_property_names_map[]
|
696
|
+
end
|
697
|
+
|
698
|
+
private def described_object_property_names_compute(**_) # TODO remove **_ eventually (keyword argument compatibility)
|
699
|
+
Set.new.tap do |property_names|
|
700
|
+
if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
|
701
|
+
property_names.merge(schema_content['properties'].keys)
|
702
|
+
end
|
703
|
+
if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
|
704
|
+
property_names.merge(schema_content['required'].to_ary)
|
705
|
+
end
|
706
|
+
end.freeze
|
707
|
+
end
|
708
|
+
|
709
|
+
# validates the given instance against this schema
|
710
|
+
#
|
711
|
+
# @param instance [Object] the instance to validate against this schema
|
712
|
+
# @return [JSI::Validation::Result]
|
713
|
+
def instance_validate(instance)
|
714
|
+
if instance.is_a?(SchemaAncestorNode)
|
715
|
+
instance_ptr = instance.jsi_ptr
|
716
|
+
instance_document = instance.jsi_document
|
717
|
+
else
|
718
|
+
instance_ptr = Ptr[]
|
719
|
+
instance_document = instance
|
720
|
+
end
|
721
|
+
internal_validate_instance(instance_ptr, instance_document)
|
722
|
+
end
|
723
|
+
|
724
|
+
# whether the given instance is valid against this schema
|
725
|
+
# @param instance [Object] the instance to validate against this schema
|
726
|
+
# @return [Boolean]
|
727
|
+
def instance_valid?(instance)
|
728
|
+
if instance.is_a?(SchemaAncestorNode)
|
729
|
+
instance = instance.jsi_node_content
|
730
|
+
end
|
731
|
+
internal_validate_instance(Ptr[], instance, validate_only: true).valid?
|
732
|
+
end
|
733
|
+
|
734
|
+
# validates the given instance against this schema
|
735
|
+
#
|
736
|
+
# @private
|
737
|
+
# @param instance_ptr [JSI::Ptr] a pointer to the instance to validate against the schema, in the instance_document
|
738
|
+
# @param instance_document [#to_hash, #to_ary, Object] document containing the instance instance_ptr pointer points to
|
739
|
+
# @param validate_only [Boolean] whether to return a full schema validation result or a simple, validation-only result
|
740
|
+
# @param visited_refs [Enumerable<JSI::Schema::Ref>]
|
741
|
+
# @return [JSI::Validation::Result]
|
742
|
+
def internal_validate_instance(instance_ptr, instance_document, validate_only: false, visited_refs: [])
|
743
|
+
if validate_only
|
744
|
+
result = JSI::Validation::VALID
|
745
|
+
else
|
746
|
+
result = JSI::Validation::FullResult.new
|
747
|
+
end
|
748
|
+
result_builder = result.builder(self, instance_ptr, instance_document, validate_only, visited_refs)
|
749
|
+
|
750
|
+
catch(:jsi_validation_result) do
|
751
|
+
# note: true/false are not valid as schemas in draft 4; they are only values of
|
752
|
+
# additionalProperties / additionalItems. since their behavior is undefined, though,
|
753
|
+
# it's fine for them to behave the same as boolean schemas in later drafts.
|
754
|
+
# I don't care about draft 4 to implement a different structuring for that.
|
755
|
+
if schema_content == true
|
756
|
+
# noop
|
757
|
+
elsif schema_content == false
|
758
|
+
result_builder.validate(false, 'instance is not valid against `false` schema')
|
759
|
+
elsif schema_content.respond_to?(:to_hash)
|
760
|
+
internal_validate_keywords(result_builder)
|
761
|
+
else
|
762
|
+
result_builder.schema_error('schema is not a boolean or a JSON object')
|
763
|
+
end
|
764
|
+
result
|
765
|
+
end.freeze
|
766
|
+
end
|
767
|
+
|
768
|
+
# schema resources which are ancestors of any subschemas below this schema.
|
769
|
+
# this may include this schema if this is a schema resource root.
|
770
|
+
# @api private
|
771
|
+
# @return [Array<JSI::Schema>]
|
772
|
+
def jsi_subschema_resource_ancestors
|
773
|
+
if schema_resource_root?
|
774
|
+
jsi_schema_resource_ancestors.dup.push(self).freeze
|
775
|
+
else
|
776
|
+
jsi_schema_resource_ancestors
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
private
|
781
|
+
|
782
|
+
def jsi_schema_initialize
|
783
|
+
@schema_ref_map = jsi_memomap(key_by: proc { |i| i[:keyword] }) do |keyword: , value: |
|
784
|
+
Schema::Ref.new(value, ref_schema: self)
|
785
|
+
end
|
786
|
+
@schema_uris_map = jsi_memomap(&method(:schema_uris_compute))
|
787
|
+
@subschema_map = jsi_memomap(&method(:subschema_compute))
|
788
|
+
@resource_root_subschema_map = jsi_memomap(&method(:resource_root_subschema_compute))
|
789
|
+
@described_object_property_names_map = jsi_memomap(&method(:described_object_property_names_compute))
|
790
|
+
end
|
791
|
+
end
|
792
|
+
end
|