jsi 0.7.0 → 0.8.0

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