jsi 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/README.md +105 -38
  5. data/lib/jsi/base.rb +349 -155
  6. data/lib/jsi/jsi_coder.rb +5 -4
  7. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  8. data/lib/jsi/metaschema_node.rb +156 -129
  9. data/lib/jsi/pathed_node.rb +47 -49
  10. data/lib/jsi/ptr.rb +292 -0
  11. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  12. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  13. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  14. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  15. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  16. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  17. data/lib/jsi/schema/application/child_application.rb +40 -0
  18. data/lib/jsi/schema/application/draft04.rb +8 -0
  19. data/lib/jsi/schema/application/draft06.rb +8 -0
  20. data/lib/jsi/schema/application/draft07.rb +8 -0
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  28. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  29. data/lib/jsi/schema/application.rb +12 -0
  30. data/lib/jsi/schema/draft04.rb +14 -0
  31. data/lib/jsi/schema/draft06.rb +14 -0
  32. data/lib/jsi/schema/draft07.rb +14 -0
  33. data/lib/jsi/schema/issue.rb +36 -0
  34. data/lib/jsi/schema/ref.rb +159 -0
  35. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  36. data/lib/jsi/schema/validation/array.rb +69 -0
  37. data/lib/jsi/schema/validation/const.rb +20 -0
  38. data/lib/jsi/schema/validation/contains.rb +25 -0
  39. data/lib/jsi/schema/validation/core.rb +39 -0
  40. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  41. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  42. data/lib/jsi/schema/validation/draft04.rb +112 -0
  43. data/lib/jsi/schema/validation/draft06.rb +122 -0
  44. data/lib/jsi/schema/validation/draft07.rb +159 -0
  45. data/lib/jsi/schema/validation/enum.rb +25 -0
  46. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  47. data/lib/jsi/schema/validation/items.rb +54 -0
  48. data/lib/jsi/schema/validation/not.rb +20 -0
  49. data/lib/jsi/schema/validation/numeric.rb +121 -0
  50. data/lib/jsi/schema/validation/object.rb +45 -0
  51. data/lib/jsi/schema/validation/pattern.rb +34 -0
  52. data/lib/jsi/schema/validation/properties.rb +101 -0
  53. data/lib/jsi/schema/validation/property_names.rb +32 -0
  54. data/lib/jsi/schema/validation/ref.rb +40 -0
  55. data/lib/jsi/schema/validation/required.rb +27 -0
  56. data/lib/jsi/schema/validation/someof.rb +90 -0
  57. data/lib/jsi/schema/validation/string.rb +47 -0
  58. data/lib/jsi/schema/validation/type.rb +49 -0
  59. data/lib/jsi/schema/validation.rb +51 -0
  60. data/lib/jsi/schema.rb +486 -133
  61. data/lib/jsi/schema_classes.rb +157 -42
  62. data/lib/jsi/schema_registry.rb +141 -0
  63. data/lib/jsi/schema_set.rb +141 -0
  64. data/lib/jsi/simple_wrap.rb +2 -2
  65. data/lib/jsi/typelike_modules.rb +52 -37
  66. data/lib/jsi/util/attr_struct.rb +106 -0
  67. data/lib/jsi/util.rb +141 -25
  68. data/lib/jsi/validation/error.rb +34 -0
  69. data/lib/jsi/validation/result.rb +210 -0
  70. data/lib/jsi/validation.rb +15 -0
  71. data/lib/jsi/version.rb +3 -1
  72. data/lib/jsi.rb +55 -9
  73. data/lib/schemas/json-schema.org/draft-04/schema.rb +8 -3
  74. data/lib/schemas/json-schema.org/draft-06/schema.rb +8 -3
  75. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  76. data/readme.rb +138 -0
  77. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  78. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  79. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  80. metadata +69 -118
  81. data/.simplecov +0 -3
  82. data/Rakefile.rb +0 -9
  83. data/jsi.gemspec +0 -28
  84. data/lib/jsi/base/to_rb.rb +0 -128
  85. data/lib/jsi/json/node.rb +0 -203
  86. data/lib/jsi/json/pointer.rb +0 -419
  87. data/lib/jsi/json-schema-fragments.rb +0 -61
  88. data/lib/jsi/json.rb +0 -10
  89. data/resources/icons/AGPL-3.0.png +0 -0
  90. data/test/base_array_test.rb +0 -323
  91. data/test/base_hash_test.rb +0 -337
  92. data/test/base_test.rb +0 -486
  93. data/test/jsi_coder_test.rb +0 -85
  94. data/test/jsi_json_arraynode_test.rb +0 -150
  95. data/test/jsi_json_hashnode_test.rb +0 -132
  96. data/test/jsi_json_node_test.rb +0 -257
  97. data/test/jsi_json_pointer_test.rb +0 -102
  98. data/test/jsi_test.rb +0 -11
  99. data/test/jsi_typelike_as_json_test.rb +0 -53
  100. data/test/metaschema_node_test.rb +0 -19
  101. data/test/schema_module_test.rb +0 -21
  102. data/test/schema_test.rb +0 -208
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -97
  105. data/test/util_test.rb +0 -62
data/lib/jsi/schema.rb CHANGED
@@ -6,6 +6,18 @@ module JSI
6
6
  # the content of an instance which is a JSI::Schema (referred to in this context as schema_content) is
7
7
  # expected to be a Hash (JSON object) or a Boolean.
8
8
  module Schema
9
+ autoload :Application, 'jsi/schema/application'
10
+ autoload :Validation, 'jsi/schema/validation'
11
+ autoload :Issue, 'jsi/schema/issue'
12
+
13
+ autoload :SchemaAncestorNode, 'jsi/schema/schema_ancestor_node'
14
+
15
+ autoload :Ref, 'jsi/schema/ref'
16
+
17
+ autoload :Draft04, 'jsi/schema/draft04'
18
+ autoload :Draft06, 'jsi/schema/draft06'
19
+ autoload :Draft07, 'jsi/schema/draft07'
20
+
9
21
  class Error < StandardError
10
22
  end
11
23
 
@@ -13,6 +25,109 @@ module JSI
13
25
  class NotASchemaError < Error
14
26
  end
15
27
 
28
+ # an exception raised when we are unable to resolve a schema reference
29
+ class ReferenceError < StandardError
30
+ end
31
+
32
+ # extends any schema which uses the keyword '$id' to identify its canonical URI
33
+ module BigMoneyId
34
+ # the contents of a $id keyword whose value is a string, or nil
35
+ # @return [#to_str, nil]
36
+ def id
37
+ if schema_content.respond_to?(:to_hash) && schema_content['$id'].respond_to?(:to_str)
38
+ schema_content['$id']
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ end
44
+
45
+ # extends any schema which uses the keyword 'id' to identify its canonical URI
46
+ module OldId
47
+ # the contents of an `id` keyword whose value is a string, or nil
48
+ # @return [#to_str, nil]
49
+ def id
50
+ if schema_content.respond_to?(:to_hash) && schema_content['id'].respond_to?(:to_str)
51
+ schema_content['id']
52
+ else
53
+ nil
54
+ end
55
+ end
56
+ end
57
+
58
+ # extends any schema which defines an anchor as a URI fragment in the schema id
59
+ module IdWithAnchor
60
+ # a URI for the schema's id, unless the id defines an anchor in its
61
+ # fragment. nil if the schema defines no id.
62
+ # @return [Addressable::URI, nil]
63
+ def id_without_fragment
64
+ if id
65
+ id_uri = Addressable::URI.parse(id)
66
+ if id_uri.merge(fragment: nil).empty?
67
+ # fragment-only id is just an anchor
68
+ # e.g. #foo
69
+ nil
70
+ elsif id_uri.fragment == nil
71
+ # no fragment
72
+ # e.g. http://localhost:1234/bar
73
+ id_uri
74
+ elsif id_uri.fragment == ''
75
+ # empty fragment
76
+ # e.g. http://json-schema.org/draft-07/schema#
77
+ id_uri.merge(fragment: nil)
78
+ elsif jsi_schema_base_uri && jsi_schema_base_uri.join(id_uri).merge(fragment: nil) == jsi_schema_base_uri
79
+ # the id, resolved against the base uri, consists of the base uri plus an anchor fragment.
80
+ # so there's no non-fragment id.
81
+ # e.g. base uri is http://localhost:1234/bar
82
+ # and id is http://localhost:1234/bar#foo
83
+ nil
84
+ else
85
+ # e.g. http://localhost:1234/bar#foo
86
+ id_uri.merge(fragment: nil)
87
+ end
88
+ else
89
+ nil
90
+ end
91
+ end
92
+
93
+ # an anchor defined by a non-empty fragment in the id uri
94
+ # @return [String]
95
+ def anchor
96
+ if id
97
+ id_uri = Addressable::URI.parse(id)
98
+ if id_uri.fragment == ''
99
+ nil
100
+ else
101
+ id_uri.fragment
102
+ end
103
+ else
104
+ nil
105
+ end
106
+ end
107
+ end
108
+
109
+ # @private
110
+ module IntegerAllows0Fraction
111
+ # is `value` an integer?
112
+ # @private
113
+ # @param value
114
+ # @return [Boolean]
115
+ def internal_integer?(value)
116
+ value.is_a?(Integer) || (value.is_a?(Numeric) && value % 1.0 == 0.0)
117
+ end
118
+ end
119
+
120
+ # @private
121
+ module IntegerDisallows0Fraction
122
+ # is `value` an integer?
123
+ # @private
124
+ # @param value
125
+ # @return [Boolean]
126
+ def internal_integer?(value)
127
+ value.is_a?(Integer)
128
+ end
129
+ end
130
+
16
131
  # JSI::Schema::DescribesSchema: a schema which describes another schema. this module
17
132
  # extends a JSI::Schema instance and indicates that JSIs which instantiate the schema
18
133
  # are themselves also schemas.
@@ -20,233 +135,471 @@ module JSI
20
135
  # examples of a schema which describes a schema include the draft JSON Schema metaschemas and
21
136
  # the OpenAPI schema definition which describes "A deterministic version of a JSON Schema object."
22
137
  module DescribesSchema
138
+ # instantiates the given schema content as a JSI Schema.
139
+ #
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.
143
+ #
144
+ # the schema will be registered with the `JSI.schema_registry`.
145
+ #
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, JSI::Schema] a JSI whose instance is the given schema_content and whose schemas
151
+ # are inplace applicators matched from self to the schema being instantiated.
152
+ def new_schema(schema_content,
153
+ uri: nil
154
+ )
155
+ schema_jsi = new_jsi(Util.deep_stringify_symbol_keys(schema_content),
156
+ uri: uri,
157
+ )
158
+ JSI.schema_registry.register(schema_jsi)
159
+ schema_jsi
160
+ end
161
+
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}.
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
170
+ end
23
171
  end
24
172
 
25
173
  class << self
26
- # @return [JSI::Schema] the default metaschema
174
+ # an application-wide default metaschema set by {default_metaschema=}, used by {JSI.new_schema}
175
+ #
176
+ # @return [nil, #new_schema]
27
177
  def default_metaschema
28
- JSI::JSONSchemaOrgDraft06.schema
178
+ return @default_metaschema if instance_variable_defined?(:@default_metaschema)
179
+ return JSONSchemaOrgDraft07
29
180
  end
30
181
 
31
- # @return [Array<JSI::Schema>] supported metaschemas
32
- def supported_metaschemas
33
- [
34
- JSI::JSONSchemaOrgDraft04.schema,
35
- JSI::JSONSchemaOrgDraft06.schema,
36
- ]
182
+ # sets an application-wide default metaschema used by {JSI.new_schema}
183
+ #
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`).
186
+ 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
37
191
  end
38
192
 
39
- # instantiates a given schema object as a JSI::Schema.
193
+ # instantiates a given schema object as a JSI Schema.
40
194
  #
41
- # schemas are instantiated according to their '$schema' property if specified. otherwise their schema
42
- # will be the {JSI::Schema.default_metaschema}.
195
+ # the metaschema to use to instantiate the schema must be indicated.
196
+ #
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
+ # - if no `$schema` property is present, the `default_metaschema` param is used, if the caller
200
+ # specifies it.
201
+ # - 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.
204
+ #
205
+ # an ArgumentError is raised if none of these indicate a metaschema to use.
206
+ #
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)`
43
210
  #
44
211
  # if the given schema_object is a JSI::Base but not already a JSI::Schema, an error
45
- # will be raised. JSI::Base _should_ already extend a given instance with JSI::Schema
46
- # when its schema describes a schema (by extending with JSI::Schema::DescribesSchema).
212
+ # will be raised. schemas which describe schemas must have JSI::Schema in their
213
+ # Schema#jsi_schema_instance_modules.
47
214
  #
48
215
  # @param schema_object [#to_hash, Boolean, JSI::Schema] an object to be instantiated as a schema.
49
- # if it's already a schema, it is returned as-is.
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`).
50
221
  # @return [JSI::Schema] a JSI::Schema representing the given schema_object
51
- def from_object(schema_object)
222
+ def new_schema(schema_object, default_metaschema: nil, **kw)
223
+ default_metaschema_new_schema = -> {
224
+ default_metaschema ||= JSI::Schema.default_metaschema
225
+ if default_metaschema.nil?
226
+ raise(ArgumentError, [
227
+ "when instantiating a schema with no `$schema` property, you must specify the metaschema.",
228
+ "you may pass the `default_metaschema` param to this method.",
229
+ "JSI::Schema.default_metaschema may be set to an application-wide default metaschema.",
230
+ "you may alternatively use new_schema on the appropriate metaschema or its schema module.",
231
+ "instantiating schema_object: #{schema_object.pretty_inspect.chomp}",
232
+ ].join("\n"))
233
+ end
234
+ if !default_metaschema.respond_to?(:new_schema)
235
+ raise(TypeError, "given default_metaschema does not respond to #new_schema: #{default_metaschema.pretty_inspect.chomp}")
236
+ end
237
+ default_metaschema.new_schema(schema_object, **kw)
238
+ }
52
239
  if schema_object.is_a?(Schema)
53
240
  schema_object
54
241
  elsif schema_object.is_a?(JSI::Base)
55
242
  raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
56
243
  elsif schema_object.respond_to?(:to_hash)
57
- schema_object = JSI.deep_stringify_symbol_keys(schema_object)
58
244
  if schema_object.key?('$schema') && schema_object['$schema'].respond_to?(:to_str)
59
- if schema_object['$schema'] == schema_object['$id'] || schema_object['$schema'] == schema_object['id']
60
- MetaschemaNode.new(schema_object)
61
- else
62
- metaschema = supported_metaschemas.detect { |ms| schema_object['$schema'] == ms['$id'] || schema_object['$schema'] == ms['id'] }
63
- unless metaschema
64
- raise(NotImplementedError, "metaschema not supported: #{schema_object['$schema']}")
65
- end
66
- metaschema.new_jsi(schema_object)
245
+ metaschema = Schema::Ref.new(schema_object['$schema']).deref_schema
246
+ unless metaschema.describes_schema?
247
+ raise(Schema::ReferenceError, "given schema_object contains a $schema but the resource it identifies does not describe a schema")
67
248
  end
249
+ metaschema.new_schema(schema_object, **kw)
68
250
  else
69
- default_metaschema.new_jsi(schema_object)
251
+ default_metaschema_new_schema.call
70
252
  end
71
253
  elsif [true, false].include?(schema_object)
72
- default_metaschema.new_jsi(schema_object)
254
+ default_metaschema_new_schema.call
73
255
  else
74
256
  raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
75
257
  end
76
258
  end
77
259
 
78
- alias_method :new, :from_object
79
- end
260
+ # @deprecated
261
+ alias_method :new, :new_schema
80
262
 
81
- # @return [String, nil] an absolute id for the schema, with a json pointer fragment. nil if
82
- # no parent of this schema defines an id.
83
- def schema_id
84
- return @schema_id if instance_variable_defined?(:@schema_id)
85
- @schema_id = begin
86
- # start from self and ascend parents looking for an 'id' property.
87
- # append a fragment to that id (appending to an existing fragment if there
88
- # is one) consisting of the path from that parent to our schema_node.
89
- node_for_id = self
90
- path_from_id_node = []
91
- done = false
263
+ # @deprecated
264
+ alias_method :from_object, :new_schema
92
265
 
93
- while !done
94
- node_content_for_id = node_for_id.node_content
95
- if node_for_id.is_a?(JSI::Schema) && node_content_for_id.respond_to?(:to_hash)
96
- parent_id = node_content_for_id.key?('$id') && node_content_for_id['$id'].respond_to?(:to_str) ? node_content_for_id['$id'].to_str :
97
- node_content_for_id.key?('id') && node_content_for_id['id'].respond_to?(:to_str) ? node_content_for_id['id'].to_str : nil
98
- end
266
+ # ensure the given object is a JSI Schema
267
+ #
268
+ # @param schema [Object] the thing the caller wishes to ensure is a Schema
269
+ # @param msg [#to_s, #to_ary] lines of the error message preceding the pretty-printed schema param
270
+ # if the schema param is not a schema
271
+ # @raise [NotASchemaError] if the schema param is not a schema
272
+ # @return [Schema] the given schema
273
+ def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
274
+ if schema.is_a?(Schema)
275
+ schema
276
+ else
277
+ if reinstantiate_as
278
+ # TODO warn; behavior is undefined and I hate this implementation
279
+
280
+ result_schema_schemas = schema.jsi_schemas + reinstantiate_as
281
+
282
+ result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_schemas)
99
283
 
100
- if parent_id || node_for_id.node_ptr.root?
101
- done = true
284
+ result_schema_class.new(Base::NOINSTANCE,
285
+ jsi_document: schema.jsi_document,
286
+ jsi_ptr: schema.jsi_ptr,
287
+ jsi_root_node: schema.jsi_root_node,
288
+ jsi_schema_base_uri: schema.jsi_schema_base_uri,
289
+ jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
290
+ )
102
291
  else
103
- path_from_id_node.unshift(node_for_id.node_ptr.reference_tokens.last)
104
- node_for_id = node_for_id.parent_node
292
+ raise(NotASchemaError, [
293
+ *msg,
294
+ schema.pretty_inspect.chomp,
295
+ ].join("\n"))
105
296
  end
106
297
  end
107
- if parent_id
108
- parent_auri = Addressable::URI.parse(parent_id)
109
- if parent_auri.fragment
110
- # add onto the fragment
111
- parent_id_path = JSI::JSON::Pointer.from_fragment(parent_auri.fragment).reference_tokens
112
- path_from_id_node = parent_id_path + path_from_id_node
113
- parent_auri.fragment = nil
114
- #else: no fragment so parent_id good as is
115
- end
298
+ end
299
+ end
116
300
 
117
- schema_id = parent_auri.merge(fragment: JSI::JSON::Pointer.new(path_from_id_node).fragment).to_s
301
+ # the underlying JSON data used to instantiate this JSI::Schema.
302
+ # this is an alias for PathedNode#jsi_node_content, named for clarity in the context of working with
303
+ # a schema.
304
+ def schema_content
305
+ jsi_node_content
306
+ end
118
307
 
119
- schema_id
308
+ # the URI of this schema, calculated from our `#id`, resolved against our `#jsi_schema_base_uri`
309
+ # @return [Addressable::URI, nil]
310
+ def schema_absolute_uri
311
+ if respond_to?(:id_without_fragment) && id_without_fragment
312
+ if jsi_schema_base_uri
313
+ Addressable::URI.parse(jsi_schema_base_uri).join(id_without_fragment)
314
+ elsif id_without_fragment.absolute?
315
+ id_without_fragment
120
316
  else
317
+ # TODO warn / schema_error
121
318
  nil
122
319
  end
123
320
  end
124
321
  end
125
322
 
126
- # @return [Module] a module representing this schema. see {JSI::SchemaClasses.module_for_schema}.
323
+ # a nonrelative URI which refers to this schema.
324
+ # nil if no parent of this schema defines an id.
325
+ # see {#schema_uris} for all URIs known to refer to this schema.
326
+ # @return [Addressable::URI, nil]
327
+ def schema_uri
328
+ schema_uris.first
329
+ end
330
+
331
+ # nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema
332
+ # @return [Array<Addressable::URI>]
333
+ def schema_uris
334
+ jsi_memoize(:schema_uris) do
335
+ each_schema_uri.to_a
336
+ end
337
+ end
338
+
339
+ # see {#schema_uris}
340
+ # @yield [Addressable::URI]
341
+ # @return [Enumerator, nil]
342
+ def each_schema_uri
343
+ return to_enum(__method__) unless block_given?
344
+
345
+ yield schema_absolute_uri if schema_absolute_uri
346
+
347
+ parent_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
348
+ resource.is_a?(Schema) && resource.schema_absolute_uri
349
+ end
350
+
351
+ anchored = self.anchor
352
+ parent_schemas.each do |parent_schema|
353
+ if anchored
354
+ if parent_schema.jsi_anchor_subschema(anchor) == self
355
+ yield parent_schema.schema_absolute_uri.merge(fragment: anchor)
356
+ else
357
+ anchored = false
358
+ end
359
+ end
360
+
361
+ relative_ptr = self.jsi_ptr.ptr_relative_to(parent_schema.jsi_ptr)
362
+ yield parent_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment)
363
+ end
364
+
365
+ nil
366
+ end
367
+
368
+ # a module which extends all instances of this schema. this may be opened by the application to add
369
+ # methods to schema instances.
370
+ #
371
+ # this module includes accessor methods for object property names this schema
372
+ # describes (see {#described_object_property_names}). these accessors wrap {Base#[]} and {Base#[]=}.
373
+ #
374
+ # some functionality is also defined on the module itself (its singleton class, not for its instances):
375
+ #
376
+ # - the module is extended with {JSI::SchemaModule}, which defines .new_jsi to instantiate instances
377
+ # of this schema (see {#new_jsi}).
378
+ # - properties described by this schema's metaschema are defined as methods to get subschemas' schema
379
+ # modules, so for example `schema.jsi_schema_module.items` returns the same module
380
+ # as `schema.items.jsi_schema_module`.
381
+ # - method .schema which returns this schema.
382
+ #
383
+ # @return [Module]
127
384
  def jsi_schema_module
128
385
  JSI::SchemaClasses.module_for_schema(self)
129
386
  end
130
387
 
131
- # @return [Class < JSI::Base] a JSI class for this one schema
388
+ # Evaluates the given block in the context of this schema's JSI schema module.
389
+ # Any arguments passed to this method will be passed to the block.
390
+ # shortcut to invoke [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec)
391
+ # on our {#jsi_schema_module}.
392
+ #
393
+ # @return the result of evaluating the block
394
+ def jsi_schema_module_exec(*a, **kw, &block)
395
+ jsi_schema_module.module_exec(*a, **kw, &block)
396
+ end
397
+
398
+ # @private @deprecated
132
399
  def jsi_schema_class
133
- JSI.class_for_schemas(Set[self])
400
+ JSI::SchemaClasses.class_for_schemas(SchemaSet[self])
134
401
  end
135
402
 
136
- # instantiates the given other_instance as a JSI::Base class for schemas matched from this schema to the
137
- # other_instance.
138
- #
139
- # any parameters are passed to JSI::Base#initialize, but none are normally used.
403
+ # instantiates the given instance as a JSI::Base class for schemas matched from this schema to the
404
+ # instance.
140
405
  #
141
- # @return [JSI::Base] a JSI whose instance is the given instance and whose schemas are matched from this
142
- # schema.
143
- def new_jsi(other_instance, *a, &b)
144
- JSI.class_for_schemas(match_to_instance(other_instance)).new(other_instance, *a, &b)
406
+ # @param instance [Object] the JSON Schema instance to be represented as a JSI
407
+ # @param uri (see SchemaSet#new_jsi)
408
+ # @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are matched
409
+ # from this schema.
410
+ def new_jsi(instance,
411
+ **kw
412
+ )
413
+ SchemaSet[self].new_jsi(instance, **kw)
145
414
  end
146
415
 
147
- # @return [Boolean] does this schema itself describe a schema?
416
+ # does this schema itself describe a schema?
417
+ # @return [Boolean]
148
418
  def describes_schema?
149
- is_a?(JSI::Schema::DescribesSchema)
419
+ jsi_schema_instance_modules.any? { |m| m <= JSI::Schema }
420
+ end
421
+
422
+ # modules to apply to instances described by this schema. these modules are included
423
+ # on this schema's {#jsi_schema_module}
424
+ # @return [Set<Module>]
425
+ def jsi_schema_instance_modules
426
+ return @jsi_schema_instance_modules if instance_variable_defined?(:@jsi_schema_instance_modules)
427
+ return Set[].freeze
428
+ end
429
+
430
+ # see {#jsi_schema_instance_modules}
431
+ #
432
+ # @return [void]
433
+ def jsi_schema_instance_modules=(jsi_schema_instance_modules)
434
+ @jsi_schema_instance_modules = Util.ensure_module_set(jsi_schema_instance_modules)
150
435
  end
151
436
 
152
- # checks this schema for applicators ($ref, allOf, etc.) which should be applied to the given instance.
153
- # returns these as a Set of {JSI::Schema}s.
437
+ # a resource containing this schema.
154
438
  #
155
- # the returned set will contain this schema itself, unless this schema contains a $ref keyword.
439
+ # if any parent, or this schema itself, is a schema with an absolute uri (see #schema_absolute_uri),
440
+ # the resource root is the closest schema with an absolute uri.
156
441
  #
157
- # @param other_instance [Object] the instance to check any applicators against
158
- # @return [Set<JSI::Schema>] matched applicator schemas
159
- def match_to_instance(other_instance)
160
- node_ptr.schema_match_ptrs_to_instance(node_document, other_instance).map do |ptr|
161
- ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
162
- end.to_set
442
+ # if no parent schema has an absolute uri, the schema_resource_root is the root of the document
443
+ # (our #jsi_root_node). in this case, the resource root may or may not be a schema itself.
444
+ #
445
+ # @return [JSI::Base] resource containing this schema
446
+ def schema_resource_root
447
+ jsi_subschema_resource_ancestors.reverse_each.detect(&:schema_resource_root?) || jsi_root_node
448
+ end
449
+
450
+ # is this schema the root of a schema resource?
451
+ # @return [Boolean]
452
+ def schema_resource_root?
453
+ jsi_ptr.root? || !!schema_absolute_uri
163
454
  end
164
455
 
165
- # returns a set of subschemas of this schema for the given property name, from keywords
166
- # `properties`, `patternProperties`, and `additionalProperties`.
456
+ # a subschema of this Schema
167
457
  #
168
- # @param property_name [String] the property name for which to find subschemas
169
- # @return [Set<JSI::Schema>] subschemas of this schema for the given property_name
170
- def subschemas_for_property_name(property_name)
171
- jsi_memoize(:subschemas_for_property_name, property_name) do |property_name|
172
- node_ptr.schema_subschema_ptrs_for_property_name(node_document, property_name).map do |ptr|
173
- ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
174
- end.to_set
458
+ # @param subptr [JSI::Ptr, #to_ary] a relative pointer, or array of tokens, pointing to the subschema
459
+ # @return [JSI::Schema] the subschema at the location indicated by subptr. self if subptr is empty.
460
+ def subschema(subptr)
461
+ subschema_map[Ptr.ary_ptr(subptr)]
462
+ end
463
+
464
+ private
465
+
466
+ def subschema_map
467
+ jsi_memomap(:subschema) do |subptr|
468
+ if is_a?(MetaschemaNode::BootstrapSchema)
469
+ self.class.new(
470
+ jsi_document,
471
+ jsi_ptr: jsi_ptr + subptr,
472
+ jsi_schema_base_uri: jsi_resource_ancestor_uri,
473
+ )
474
+ else
475
+ Schema.ensure_schema(subptr.evaluate(self, as_jsi: true), msg: [
476
+ "subschema is not a schema at pointer: #{subptr.pointer}"
477
+ ])
478
+ end
175
479
  end
176
480
  end
177
481
 
178
- # returns a set of subschemas of this schema for the given array index, from keywords
179
- # `items` and `additionalItems`.
482
+ public
483
+
484
+ # a schema in the same schema resource as this one (see #schema_resource_root) at the given
485
+ # pointer relative to the root of the schema resource.
180
486
  #
181
- # @param index [Integer] the array index for which to find subschemas
182
- # @return [Set<JSI::Schema>] subschemas of this schema for the given array index
183
- def subschemas_for_index(index)
184
- jsi_memoize(:subschemas_for_index, index) do |index|
185
- node_ptr.schema_subschema_ptrs_for_index(node_document, index).map do |ptr|
186
- ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
187
- end.to_set
487
+ # @param ptr [JSI::Ptr, #to_ary] a pointer to a schema from our schema resource root
488
+ # @return [JSI::Schema] the schema pointed to by ptr
489
+ def resource_root_subschema(ptr)
490
+ resource_root_subschema_map[Ptr.ary_ptr(ptr)]
491
+ end
492
+
493
+ private
494
+
495
+ def resource_root_subschema_map
496
+ jsi_memomap(:resource_root_subschema_map) do |ptr|
497
+ schema = self
498
+ if schema.is_a?(MetaschemaNode::BootstrapSchema)
499
+ # BootstrapSchema does not track jsi_schema_resource_ancestors used by #schema_resource_root;
500
+ # resource_root_subschema is always relative to the document root.
501
+ # BootstrapSchema also does not implement jsi_root_node or #[]. we instantiate the ptr directly
502
+ # rather than as a subschema from the root.
503
+ schema.class.new(
504
+ schema.jsi_document,
505
+ jsi_ptr: ptr,
506
+ jsi_schema_base_uri: nil,
507
+ )
508
+ else
509
+ resource_root = schema.schema_resource_root
510
+ Schema.ensure_schema(ptr.evaluate(resource_root, as_jsi: true),
511
+ msg: [
512
+ "subschema is not a schema at pointer: #{ptr.pointer}"
513
+ ],
514
+ reinstantiate_as: schema.jsi_schemas.select(&:describes_schema?)
515
+ )
516
+ end
188
517
  end
189
518
  end
190
519
 
191
- # @return [Set] any object property names this schema indicates may be present on its instances.
192
- # this includes any keys of this schema's "properties" object and any entries of this schema's
193
- # array of "required" property keys.
520
+ public
521
+
522
+ # any object property names this schema indicates may be present on its instances.
523
+ # this includes any keys of this schema's "properties" object and any entries of this schema's
524
+ # array of "required" property keys.
525
+ # @return [Set]
194
526
  def described_object_property_names
195
527
  jsi_memoize(:described_object_property_names) do
196
528
  Set.new.tap do |property_names|
197
- if node_content.respond_to?(:to_hash) && node_content['properties'].respond_to?(:to_hash)
198
- property_names.merge(node_content['properties'].keys)
529
+ if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
530
+ property_names.merge(schema_content['properties'].keys)
199
531
  end
200
- if node_content.respond_to?(:to_hash) && node_content['required'].respond_to?(:to_ary)
201
- property_names.merge(node_content['required'].to_ary)
532
+ if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
533
+ property_names.merge(schema_content['required'].to_ary)
202
534
  end
203
- end
535
+ end.freeze
204
536
  end
205
537
  end
206
538
 
207
- # @return [Array] array of schema validation errors for
208
- # the given instance against this schema
539
+ # validates the given instance against this schema
540
+ #
541
+ # @param instance [Object] the instance to validate against this schema
542
+ # @return [JSI::Validation::Result]
543
+ def instance_validate(instance)
544
+ if instance.is_a?(JSI::PathedNode)
545
+ instance_ptr = instance.jsi_ptr
546
+ instance_document = instance.jsi_document
547
+ else
548
+ instance_ptr = Ptr[]
549
+ instance_document = instance
550
+ end
551
+ internal_validate_instance(instance_ptr, instance_document)
552
+ end
553
+
554
+ # whether the given instance is valid against this schema
555
+ # @param instance [Object] the instance to validate against this schema
556
+ # @return [Boolean]
557
+ def instance_valid?(instance)
558
+ if instance.is_a?(JSI::PathedNode)
559
+ instance = instance.jsi_node_content
560
+ end
561
+ internal_validate_instance(Ptr[], instance, validate_only: true).valid?
562
+ end
563
+
564
+ # @private
209
565
  def fully_validate_instance(other_instance, errors_as_objects: false)
210
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment, errors_as_objects: errors_as_objects)
566
+ raise(NotImplementedError, "Schema#fully_validate_instance removed: see new validation interface Schema#instance_validate")
211
567
  end
212
568
 
213
- # @return [true, false] whether the given instance validates against this schema
569
+ # @private
214
570
  def validate_instance(other_instance)
215
- ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
571
+ raise(NotImplementedError, "Schema#validate_instance renamed: see Schema#instance_valid?")
216
572
  end
217
573
 
218
- # @return [true] if this method does not raise, it returns true to
219
- # indicate the instance is valid against this schema
220
- # @raise [::JSON::Schema::ValidationError] raises if the instance has
221
- # validation errors against this schema
574
+ # @private
222
575
  def validate_instance!(other_instance)
223
- ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
576
+ raise(NotImplementedError, "Schema#validate_instance! removed")
224
577
  end
225
578
 
226
- # @return [Array] array of schema validation errors for
227
- # this schema, validated against its metaschema. a default metaschema
228
- # is assumed if the schema does not specify a $schema.
579
+ # @private
229
580
  def fully_validate_schema(errors_as_objects: false)
230
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true, errors_as_objects: errors_as_objects)
581
+ raise(NotImplementedError, "Schema#fully_validate_schema removed: use validation interface Base#jsi_validate on the schema")
231
582
  end
232
583
 
233
- # @return [true, false] whether this schema validates against its metaschema
584
+ # @private
234
585
  def validate_schema
235
- ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
586
+ raise(NotImplementedError, "Schema#validate_schema removed: use validation interface Base#jsi_valid? on the schema")
236
587
  end
237
588
 
238
- # @return [true] if this method does not raise, it returns true to
239
- # indicate this schema is valid against its metaschema
240
- # @raise [::JSON::Schema::ValidationError] raises if this schema has
241
- # validation errors against its metaschema
589
+ # @private
242
590
  def validate_schema!
243
- ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
591
+ raise(NotImplementedError, "Schema#validate_schema! removed")
244
592
  end
245
593
 
246
- private
247
- def jsi_ensure_subschema_is_schema(subschema, ptr)
248
- unless subschema.is_a?(JSI::Schema)
249
- raise(NotASchemaError, "subschema not a schema at ptr #{ptr.inspect}: #{subschema.pretty_inspect.chomp}")
594
+ # schema resources which are ancestors of any subschemas below this schema.
595
+ # this may include this schema if this is a schema resource root.
596
+ # @private
597
+ # @return [Array<JSI::Schema>]
598
+ def jsi_subschema_resource_ancestors
599
+ if schema_resource_root?
600
+ jsi_schema_resource_ancestors + [self]
601
+ else
602
+ jsi_schema_resource_ancestors
250
603
  end
251
604
  end
252
605
  end