jsi 0.4.0 → 0.7.0

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