jsi-dev 0.0.2

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