jsi 0.7.0 → 0.8.1

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