jsi 0.4.0 → 0.6.0

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