jsi 0.7.0 → 0.8.0

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