jsi 0.7.0 → 0.8.1

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 (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