jsi 0.4.0 → 0.7.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. 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
+
12
+ autoload :Issue, 'jsi/schema/issue'
13
+ autoload :Ref, 'jsi/schema/ref'
14
+
15
+ autoload :SchemaAncestorNode, 'jsi/schema/schema_ancestor_node'
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 keyword?('$id') && 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 keyword?('id') && 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,477 @@ 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] a JSI which is a {JSI::Schema} whose instance is the given `schema_content`
151
+ # and whose schemas are this schema's inplace applicators.
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 nil
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.
194
+ #
195
+ # the metaschema to use to instantiate the schema must be indicated.
40
196
  #
41
- # schemas are instantiated according to their '$schema' property if specified. otherwise their schema
42
- # will be the {JSI::Schema.default_metaschema}.
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 include JSI::Schema in their
213
+ # {Schema#jsi_schema_module}.
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.
50
- # @return [JSI::Schema] a JSI::Schema representing the given schema_object
51
- def from_object(schema_object)
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)
224
+ default_metaschema_new_schema = -> {
225
+ default_metaschema ||= JSI::Schema.default_metaschema
226
+ if default_metaschema.nil?
227
+ 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}",
233
+ ].join("\n"))
234
+ 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)
239
+ }
52
240
  if schema_object.is_a?(Schema)
53
241
  schema_object
54
242
  elsif schema_object.is_a?(JSI::Base)
55
243
  raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
56
244
  elsif schema_object.respond_to?(:to_hash)
57
- schema_object = JSI.deep_stringify_symbol_keys(schema_object)
58
- 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
+ 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")
67
248
  end
249
+ metaschema = Schema::Ref.new(schema_object['$schema']).deref_schema
250
+ unless metaschema.describes_schema?
251
+ raise(Schema::ReferenceError, "given schema_object contains a $schema but the resource it identifies does not describe a schema")
252
+ end
253
+ metaschema.new_schema(schema_object, **kw)
68
254
  else
69
- default_metaschema.new_jsi(schema_object)
255
+ default_metaschema_new_schema.call
70
256
  end
71
257
  elsif [true, false].include?(schema_object)
72
- default_metaschema.new_jsi(schema_object)
258
+ default_metaschema_new_schema.call
73
259
  else
74
260
  raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
75
261
  end
76
262
  end
77
263
 
78
- alias_method :new, :from_object
79
- end
264
+ # ensure the given object is a JSI Schema
265
+ #
266
+ # @param schema [Object] the thing the caller wishes to ensure is a Schema
267
+ # @param msg [#to_s, #to_ary] lines of the error message preceding the pretty-printed schema param
268
+ # if the schema param is not a schema
269
+ # @raise [NotASchemaError] if the schema param is not a schema
270
+ # @return [Schema] the given schema
271
+ def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
272
+ if schema.is_a?(Schema)
273
+ schema
274
+ else
275
+ if reinstantiate_as
276
+ # TODO warn; behavior is undefined and I hate this implementation
80
277
 
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
278
+ result_schema_schemas = schema.jsi_schemas + reinstantiate_as
92
279
 
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
280
+ result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_schemas,
281
+ includes: SchemaClasses.includes_for(schema.jsi_node_content)
282
+ )
99
283
 
100
- if parent_id || node_for_id.node_ptr.root?
101
- done = true
284
+ result_schema_class.new(schema.jsi_document,
285
+ jsi_ptr: schema.jsi_ptr,
286
+ jsi_root_node: schema.jsi_root_node,
287
+ jsi_schema_base_uri: schema.jsi_schema_base_uri,
288
+ jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
289
+ )
102
290
  else
103
- path_from_id_node.unshift(node_for_id.node_ptr.reference_tokens.last)
104
- node_for_id = node_for_id.parent_node
291
+ raise(NotASchemaError, [
292
+ *msg,
293
+ schema.pretty_inspect.chomp,
294
+ ].join("\n"))
105
295
  end
106
296
  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
297
+ end
298
+ end
116
299
 
117
- schema_id = parent_auri.merge(fragment: JSI::JSON::Pointer.new(path_from_id_node).fragment).to_s
300
+ # the underlying JSON data used to instantiate this JSI::Schema.
301
+ # this is an alias for {Base#jsi_node_content}, named for clarity in the context of working with
302
+ # a schema.
303
+ def schema_content
304
+ jsi_node_content
305
+ end
306
+
307
+ # does this schema contain the given keyword?
308
+ # @return [Boolean]
309
+ def keyword?(keyword)
310
+ schema_content = jsi_node_content
311
+ schema_content.respond_to?(:to_hash) && schema_content.key?(keyword)
312
+ end
118
313
 
119
- schema_id
314
+ # the URI of this schema, calculated from our `#id`, resolved against our `#jsi_schema_base_uri`
315
+ # @return [Addressable::URI, nil]
316
+ def schema_absolute_uri
317
+ if respond_to?(:id_without_fragment) && id_without_fragment
318
+ if jsi_schema_base_uri
319
+ jsi_schema_base_uri.join(id_without_fragment)
320
+ elsif id_without_fragment.absolute?
321
+ id_without_fragment
120
322
  else
323
+ # TODO warn / schema_error
121
324
  nil
122
325
  end
123
326
  end
124
327
  end
125
328
 
126
- # @return [Module] a module representing this schema. see {JSI::SchemaClasses.module_for_schema}.
329
+ # a nonrelative URI which refers to this schema.
330
+ # nil if no parent of this schema defines an id.
331
+ # see {#schema_uris} for all URIs known to refer to this schema.
332
+ # @return [Addressable::URI, nil]
333
+ def schema_uri
334
+ schema_uris.first
335
+ end
336
+
337
+ # nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema
338
+ # @return [Array<Addressable::URI>]
339
+ def schema_uris
340
+ jsi_memoize(:schema_uris) do
341
+ each_schema_uri.to_a
342
+ end
343
+ end
344
+
345
+ # see {#schema_uris}
346
+ # @yield [Addressable::URI]
347
+ # @return [Enumerator, nil]
348
+ def each_schema_uri
349
+ return to_enum(__method__) unless block_given?
350
+
351
+ yield schema_absolute_uri if schema_absolute_uri
352
+
353
+ parent_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
354
+ resource.schema_absolute_uri
355
+ end
356
+
357
+ anchored = respond_to?(:anchor) ? anchor : nil
358
+ parent_schemas.each do |parent_schema|
359
+ if anchored
360
+ if parent_schema.jsi_anchor_subschema(anchor) == self
361
+ yield parent_schema.schema_absolute_uri.merge(fragment: anchor)
362
+ else
363
+ anchored = false
364
+ end
365
+ end
366
+
367
+ relative_ptr = jsi_ptr.relative_to(parent_schema.jsi_ptr)
368
+ yield parent_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment)
369
+ end
370
+
371
+ nil
372
+ end
373
+
374
+ # a module which extends all instances of this schema. this may be opened by the application to add
375
+ # methods to schema instances.
376
+ #
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
+ # some functionality is also defined on the module itself (its singleton class, not for its instances):
381
+ #
382
+ # - the module is extended with {JSI::SchemaModule}, which defines .new_jsi to instantiate instances
383
+ # of this schema (see {#new_jsi}).
384
+ # - properties described by this schema's metaschema are defined as methods to get subschemas' schema
385
+ # modules, so for example `schema.jsi_schema_module.items` returns the same module
386
+ # as `schema.items.jsi_schema_module`.
387
+ # - method .schema which returns this schema.
388
+ #
389
+ # @return [Module]
127
390
  def jsi_schema_module
128
391
  JSI::SchemaClasses.module_for_schema(self)
129
392
  end
130
393
 
131
- # @return [Class < JSI::Base] a JSI class for this one schema
132
- def jsi_schema_class
133
- JSI.class_for_schemas(Set[self])
394
+ # Evaluates the given block in the context of this schema's JSI schema module.
395
+ # Any arguments passed to this method will be passed to the block.
396
+ # shortcut to invoke [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec)
397
+ # on our {#jsi_schema_module}.
398
+ #
399
+ # @return the result of evaluating the block
400
+ def jsi_schema_module_exec(*a, **kw, &block)
401
+ jsi_schema_module.module_exec(*a, **kw, &block)
134
402
  end
135
403
 
136
- # instantiates the given other_instance as a JSI::Base class for schemas matched from this schema to the
137
- # other_instance.
404
+ # instantiates the given instance as a JSI::Base class for schemas matched from this schema to the
405
+ # instance.
138
406
  #
139
- # any parameters are passed to JSI::Base#initialize, but none are normally used.
140
- #
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)
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.
411
+ def new_jsi(instance, **kw)
412
+ SchemaSet[self].new_jsi(instance, **kw)
145
413
  end
146
414
 
147
- # @return [Boolean] does this schema itself describe a schema?
415
+ # does this schema itself describe a schema?
416
+ # @return [Boolean]
148
417
  def describes_schema?
149
- is_a?(JSI::Schema::DescribesSchema)
418
+ jsi_schema_module <= JSI::Schema ||
419
+ # deprecated
420
+ jsi_schema_instance_modules.any? { |m| m <= JSI::Schema }
150
421
  end
151
422
 
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.
154
- #
155
- # the returned set will contain this schema itself, unless this schema contains a $ref keyword.
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
430
+ end
431
+
432
+ # see {#jsi_schema_instance_modules}
156
433
  #
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
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)
163
438
  end
164
439
 
165
- # returns a set of subschemas of this schema for the given property name, from keywords
166
- # `properties`, `patternProperties`, and `additionalProperties`.
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.
167
443
  #
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
444
+ # @param schema_implementation_modules [Enumerable<Module>] modules which implement the functionality of
445
+ # the schema to extend schemas described by this schema.
446
+ # this must include JSI::Schema (usually indirectly).
447
+ # @return [void]
448
+ def describes_schema!(schema_implementation_modules)
449
+ schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)
450
+
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
+ if describes_schema?
456
+ # this schema, or one equal to it, has already had describes_schema! called on it.
457
+ # this is to be avoided, but is not particularly a problem.
458
+ # it is a bug if it was called different times with different schema_implementation_modules, though.
459
+ unless jsi_schema_module.schema_implementation_modules == schema_implementation_modules
460
+ raise(ArgumentError, "this schema already describes a schema with different schema_implementation_modules")
461
+ end
462
+ else
463
+ schema_implementation_modules.each do |mod|
464
+ jsi_schema_module.include(mod)
465
+ end
466
+ jsi_schema_module.extend(DescribesSchemaModule)
467
+ jsi_schema_module.instance_variable_set(:@schema_implementation_modules, schema_implementation_modules)
175
468
  end
469
+
470
+ extend(DescribesSchema)
471
+
472
+ nil
176
473
  end
177
474
 
178
- # returns a set of subschemas of this schema for the given array index, from keywords
179
- # `items` and `additionalItems`.
475
+ # a resource containing this schema.
180
476
  #
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
188
- end
477
+ # if any parent, or this schema itself, is a schema with an absolute uri (see {#schema_absolute_uri}),
478
+ # the resource root is the closest schema with an absolute uri.
479
+ #
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.
482
+ #
483
+ # @return [JSI::Base] resource containing this schema
484
+ def schema_resource_root
485
+ jsi_subschema_resource_ancestors.reverse_each.detect(&:schema_resource_root?) || jsi_root_node
189
486
  end
190
487
 
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.
194
- def described_object_property_names
195
- jsi_memoize(:described_object_property_names) do
196
- 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)
199
- 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)
202
- end
488
+ # is this schema the root of a schema resource?
489
+ # @return [Boolean]
490
+ def schema_resource_root?
491
+ jsi_ptr.root? || !!schema_absolute_uri
492
+ end
493
+
494
+ # a subschema of this Schema
495
+ #
496
+ # @param subptr [JSI::Ptr, #to_ary] a relative pointer, or array of tokens, pointing to the subschema
497
+ # @return [JSI::Schema] the subschema at the location indicated by subptr. self if subptr is empty.
498
+ 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
513
+ Schema.ensure_schema(jsi_descendent_node(subptr), msg: [
514
+ "subschema is not a schema at pointer: #{subptr.pointer}"
515
+ ])
203
516
  end
204
517
  end
205
518
  end
206
519
 
207
- # @return [Array] array of schema validation errors for
208
- # the given instance against this schema
209
- 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)
211
- end
520
+ public
212
521
 
213
- # @return [true, false] whether the given instance validates against this schema
214
- 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)
522
+ # a schema in the same schema resource as this one (see {#schema_resource_root}) at the given
523
+ # pointer relative to the root of the schema resource.
524
+ #
525
+ # @param ptr [JSI::Ptr, #to_ary] a pointer to a schema from our schema resource root
526
+ # @return [JSI::Schema] the schema pointed to by ptr
527
+ def resource_root_subschema(ptr)
528
+ resource_root_subschema_map[ptr: Ptr.ary_ptr(ptr)]
216
529
  end
217
530
 
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
222
- 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)
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
546
+ Schema.ensure_schema(schema_resource_root.jsi_descendent_node(ptr),
547
+ msg: [
548
+ "subschema is not a schema at pointer: #{ptr.pointer}"
549
+ ],
550
+ reinstantiate_as: jsi_schemas.select(&:describes_schema?)
551
+ )
552
+ end
553
+ end
224
554
  end
225
555
 
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.
229
- 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)
556
+ public
557
+
558
+ # any object property names this schema indicates may be present on its instances.
559
+ # this includes any keys of this schema's "properties" object and any entries of this schema's
560
+ # array of "required" property keys.
561
+ # @return [Set]
562
+ def described_object_property_names
563
+ jsi_memoize(:described_object_property_names) do
564
+ Set.new.tap do |property_names|
565
+ if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
566
+ property_names.merge(schema_content['properties'].keys)
567
+ end
568
+ if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
569
+ property_names.merge(schema_content['required'].to_ary)
570
+ end
571
+ end.freeze
572
+ end
231
573
  end
232
574
 
233
- # @return [true, false] whether this schema validates against its metaschema
234
- def validate_schema
235
- ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
575
+ # validates the given instance against this schema
576
+ #
577
+ # @param instance [Object] the instance to validate against this schema
578
+ # @return [JSI::Validation::Result]
579
+ def instance_validate(instance)
580
+ if instance.is_a?(Base)
581
+ instance_ptr = instance.jsi_ptr
582
+ instance_document = instance.jsi_document
583
+ else
584
+ instance_ptr = Ptr[]
585
+ instance_document = instance
586
+ end
587
+ internal_validate_instance(instance_ptr, instance_document)
236
588
  end
237
589
 
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
242
- def validate_schema!
243
- ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
590
+ # whether the given instance is valid against this schema
591
+ # @param instance [Object] the instance to validate against this schema
592
+ # @return [Boolean]
593
+ def instance_valid?(instance)
594
+ if instance.is_a?(Base)
595
+ instance = instance.jsi_node_content
596
+ end
597
+ internal_validate_instance(Ptr[], instance, validate_only: true).valid?
244
598
  end
245
599
 
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}")
600
+ # schema resources which are ancestors of any subschemas below this schema.
601
+ # this may include this schema if this is a schema resource root.
602
+ # @api private
603
+ # @return [Array<JSI::Schema>]
604
+ def jsi_subschema_resource_ancestors
605
+ if schema_resource_root?
606
+ jsi_schema_resource_ancestors.dup.push(self).freeze
607
+ else
608
+ jsi_schema_resource_ancestors
250
609
  end
251
610
  end
252
611
  end