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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +101 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +303 -0
  6. data/docs/glossary.md +281 -0
  7. data/jsi.gemspec +30 -0
  8. data/lib/jsi/base/node.rb +373 -0
  9. data/lib/jsi/base.rb +738 -0
  10. data/lib/jsi/jsi_coder.rb +92 -0
  11. data/lib/jsi/metaschema.rb +6 -0
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
  13. data/lib/jsi/metaschema_node.rb +262 -0
  14. data/lib/jsi/ptr.rb +314 -0
  15. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  16. data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
  17. data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
  18. data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
  19. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  20. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  21. data/lib/jsi/schema/application/child_application.rb +13 -0
  22. data/lib/jsi/schema/application/draft04.rb +8 -0
  23. data/lib/jsi/schema/application/draft06.rb +8 -0
  24. data/lib/jsi/schema/application/draft07.rb +8 -0
  25. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  26. data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
  27. data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
  28. data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
  29. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  30. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  31. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  32. data/lib/jsi/schema/application/inplace_application.rb +14 -0
  33. data/lib/jsi/schema/application.rb +12 -0
  34. data/lib/jsi/schema/draft04.rb +13 -0
  35. data/lib/jsi/schema/draft06.rb +13 -0
  36. data/lib/jsi/schema/draft07.rb +13 -0
  37. data/lib/jsi/schema/issue.rb +36 -0
  38. data/lib/jsi/schema/ref.rb +183 -0
  39. data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
  40. data/lib/jsi/schema/validation/array.rb +69 -0
  41. data/lib/jsi/schema/validation/const.rb +20 -0
  42. data/lib/jsi/schema/validation/contains.rb +25 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +110 -0
  46. data/lib/jsi/schema/validation/draft06.rb +120 -0
  47. data/lib/jsi/schema/validation/draft07.rb +157 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +49 -0
  63. data/lib/jsi/schema.rb +792 -0
  64. data/lib/jsi/schema_classes.rb +357 -0
  65. data/lib/jsi/schema_registry.rb +190 -0
  66. data/lib/jsi/schema_set.rb +219 -0
  67. data/lib/jsi/simple_wrap.rb +26 -0
  68. data/lib/jsi/util/private/attr_struct.rb +130 -0
  69. data/lib/jsi/util/private/memo_map.rb +75 -0
  70. data/lib/jsi/util/private.rb +202 -0
  71. data/lib/jsi/util/typelike.rb +225 -0
  72. data/lib/jsi/util.rb +227 -0
  73. data/lib/jsi/validation/error.rb +34 -0
  74. data/lib/jsi/validation/result.rb +212 -0
  75. data/lib/jsi/validation.rb +15 -0
  76. data/lib/jsi/version.rb +5 -0
  77. data/lib/jsi.rb +105 -0
  78. data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
  79. data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
  80. data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
  81. data/readme.rb +138 -0
  82. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  83. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  84. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  85. 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