jsi-dev 0.0.0.pre.commonmarker

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +101 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +303 -0
  6. data/docs/glossary.md +281 -0
  7. data/jsi.gemspec +30 -0
  8. data/lib/jsi/base/node.rb +373 -0
  9. data/lib/jsi/base.rb +738 -0
  10. data/lib/jsi/jsi_coder.rb +92 -0
  11. data/lib/jsi/metaschema.rb +6 -0
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
  13. data/lib/jsi/metaschema_node.rb +262 -0
  14. data/lib/jsi/ptr.rb +314 -0
  15. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  16. data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
  17. data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
  18. data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
  19. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  20. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  21. data/lib/jsi/schema/application/child_application.rb +13 -0
  22. data/lib/jsi/schema/application/draft04.rb +8 -0
  23. data/lib/jsi/schema/application/draft06.rb +8 -0
  24. data/lib/jsi/schema/application/draft07.rb +8 -0
  25. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  26. data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
  27. data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
  28. data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
  29. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  30. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  31. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  32. data/lib/jsi/schema/application/inplace_application.rb +14 -0
  33. data/lib/jsi/schema/application.rb +12 -0
  34. data/lib/jsi/schema/draft04.rb +13 -0
  35. data/lib/jsi/schema/draft06.rb +13 -0
  36. data/lib/jsi/schema/draft07.rb +13 -0
  37. data/lib/jsi/schema/issue.rb +36 -0
  38. data/lib/jsi/schema/ref.rb +183 -0
  39. data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
  40. data/lib/jsi/schema/validation/array.rb +69 -0
  41. data/lib/jsi/schema/validation/const.rb +20 -0
  42. data/lib/jsi/schema/validation/contains.rb +25 -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 +110 -0
  46. data/lib/jsi/schema/validation/draft06.rb +120 -0
  47. data/lib/jsi/schema/validation/draft07.rb +157 -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 +49 -0
  63. data/lib/jsi/schema.rb +792 -0
  64. data/lib/jsi/schema_classes.rb +357 -0
  65. data/lib/jsi/schema_registry.rb +190 -0
  66. data/lib/jsi/schema_set.rb +219 -0
  67. data/lib/jsi/simple_wrap.rb +26 -0
  68. data/lib/jsi/util/private/attr_struct.rb +130 -0
  69. data/lib/jsi/util/private/memo_map.rb +75 -0
  70. data/lib/jsi/util/private.rb +202 -0
  71. data/lib/jsi/util/typelike.rb +225 -0
  72. data/lib/jsi/util.rb +227 -0
  73. data/lib/jsi/validation/error.rb +34 -0
  74. data/lib/jsi/validation/result.rb +212 -0
  75. data/lib/jsi/validation.rb +15 -0
  76. data/lib/jsi/version.rb +5 -0
  77. data/lib/jsi.rb +105 -0
  78. data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
  79. data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
  80. data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
  81. data/readme.rb +138 -0
  82. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  83. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  84. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  85. metadata +155 -0
data/lib/jsi/base.rb ADDED
@@ -0,0 +1,738 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # A JSI::Base instance represents a node in a JSON document (its {#jsi_document}) at a particular
5
+ # location (its {#jsi_ptr}), described by any number of JSON Schemas (its {#jsi_schemas}).
6
+ #
7
+ # JSI::Base is an abstract base class. The subclasses used to instantiate JSIs are dynamically created as
8
+ # needed for a given instance.
9
+ #
10
+ # These subclasses are generally intended to be ignored by applications using this library - the purpose
11
+ # they serve is to include modules relevant to the instance. The modules these classes include are:
12
+ #
13
+ # - the {Schema#jsi_schema_module} of each schema which describes the instance
14
+ # - {Base::HashNode}, {Base::ArrayNode}, or {Base::StringNode} if the instance is
15
+ # a hash/object, array, or string
16
+ # - Modules defining accessor methods for property names described by the schemas
17
+ class Base
18
+ autoload :ArrayNode, 'jsi/base/node'
19
+ autoload :HashNode, 'jsi/base/node'
20
+ autoload :StringNode, 'jsi/base/node'
21
+
22
+ include Schema::SchemaAncestorNode
23
+
24
+ # An exception raised when attempting to access a child of a node which cannot have children.
25
+ # A complex node can have children, a simple node cannot.
26
+ class SimpleNodeChildError < StandardError
27
+ end
28
+
29
+ class << self
30
+ # A string indicating the schema module name
31
+ # and/or schema URI of each schema the class represents.
32
+ # @return [String]
33
+ def inspect
34
+ if !respond_to?(:jsi_class_schemas)
35
+ super
36
+ else
37
+ schema_names = jsi_class_schemas.map do |schema|
38
+ mod_name = schema.jsi_schema_module.name_from_ancestor
39
+ if mod_name && schema.schema_absolute_uri
40
+ "#{mod_name} <#{schema.schema_absolute_uri}>"
41
+ elsif mod_name
42
+ mod_name
43
+ elsif schema.schema_uri
44
+ schema.schema_uri.to_s
45
+ else
46
+ schema.jsi_ptr.uri.to_s
47
+ end
48
+ end
49
+
50
+ if schema_names.empty?
51
+ "(JSI Schema Class for 0 schemas)"
52
+ else
53
+ -"(JSI Schema Class: #{schema_names.join(' + ')})"
54
+ end
55
+ end
56
+ end
57
+
58
+ alias_method :to_s, :inspect
59
+ # A constant name of this class. This is generated from any schema module name or URI of each schema
60
+ # this class represents, or random characters.
61
+ #
62
+ # this generated name is not too pretty but can be more helpful than an anonymous class, especially
63
+ # in error messages.
64
+ #
65
+ # @return [String]
66
+ def name
67
+ return super if instance_variable_defined?(:@tried_to_name)
68
+ @tried_to_name = true
69
+ return super unless respond_to?(:jsi_class_schemas)
70
+ alnum = proc { |id| (id % 36**4).to_s(36).rjust(4, '0').upcase }
71
+ schema_names = jsi_class_schemas.map do |schema|
72
+ named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
73
+ if named_ancestor_schema
74
+ [named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
75
+ elsif schema.schema_uri
76
+ schema.schema_uri.to_s
77
+ else
78
+ [alnum[schema.jsi_root_node.__id__], *schema.jsi_ptr.tokens].join('_')
79
+ end
80
+ end
81
+ includes_names = jsi_class_includes.map { |m| m.name.sub(/\AJSI::Base::/, '').gsub(Util::RUBY_REJECT_NAME_RE, '_') }
82
+ if schema_names.any?
83
+ parts = schema_names.compact.sort.map { |n| 'X' + n.to_s }
84
+ parts += includes_names
85
+ const_name = Util.const_name_from_parts(parts, join: '__')
86
+ const_name += "__" + alnum[__id__] if SchemaClasses.const_defined?(const_name)
87
+ else
88
+ const_name = (['X' + alnum[__id__]] + includes_names).join('__')
89
+ end
90
+ # collisions are technically possible though vanishingly unlikely
91
+ SchemaClasses.const_set(const_name, self) unless SchemaClasses.const_defined?(const_name)
92
+ super
93
+ end
94
+ end
95
+
96
+ # initializes a JSI whose instance is in the given document at the given pointer.
97
+ #
98
+ # this is a private api - users should look elsewhere to instantiate JSIs, in particular:
99
+ #
100
+ # - {JSI.new_schema} and {Schema::DescribesSchema#new_schema} to instantiate schemas
101
+ # - {Schema#new_jsi} to instantiate schema instances
102
+ #
103
+ # @api private
104
+ # @param jsi_document [Object] the document containing the instance
105
+ # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
106
+ # @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
107
+ # @param jsi_schema_resource_ancestors [Array<JSI::Base + JSI::Schema>]
108
+ # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
109
+ def initialize(jsi_document,
110
+ jsi_ptr: Ptr[],
111
+ jsi_indicated_schemas: ,
112
+ jsi_schema_base_uri: nil,
113
+ jsi_schema_resource_ancestors: Util::EMPTY_ARY,
114
+ jsi_schema_registry: ,
115
+ jsi_root_node: nil
116
+ )
117
+ raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
118
+
119
+ super()
120
+
121
+ self.jsi_document = jsi_document
122
+ self.jsi_ptr = jsi_ptr
123
+ self.jsi_indicated_schemas = jsi_indicated_schemas
124
+ self.jsi_schema_base_uri = jsi_schema_base_uri
125
+ self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
126
+ self.jsi_schema_registry = jsi_schema_registry
127
+ if @jsi_ptr.root?
128
+ raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
129
+ @jsi_root_node = self
130
+ else
131
+ raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
132
+ raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
133
+ @jsi_root_node = jsi_root_node
134
+ end
135
+
136
+ jsi_memomaps_initialize
137
+
138
+ if jsi_instance.is_a?(JSI::Base)
139
+ raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
140
+ end
141
+ end
142
+
143
+ # @!method jsi_schemas
144
+ # The set of schemas that describe this instance.
145
+ # These are the applicator schemas that apply to this instance, the result of inplace application
146
+ # of our {#jsi_indicated_schemas}.
147
+ # @return [JSI::SchemaSet]
148
+ # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
149
+
150
+
151
+ # document containing the instance of this JSI at our {#jsi_ptr}
152
+ attr_reader :jsi_document
153
+
154
+ # {JSI::Ptr} pointing to this JSI's instance within our {#jsi_document}
155
+ # @return [JSI::Ptr]
156
+ attr_reader :jsi_ptr
157
+
158
+ # the JSI at the root of this JSI's document
159
+ # @return [JSI::Base]
160
+ attr_reader :jsi_root_node
161
+
162
+ # the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
163
+ def jsi_node_content
164
+ content = jsi_ptr.evaluate(jsi_document)
165
+ content
166
+ end
167
+
168
+ # the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
169
+ alias_method :jsi_instance, :jsi_node_content
170
+
171
+ # the schemas indicated as describing this instance, prior to inplace application.
172
+ #
173
+ # this is different from {#jsi_schemas}, which are the inplace applicator schemas
174
+ # which describe this instance. for most purposes, `#jsi_schemas` is more relevant.
175
+ #
176
+ # `jsi_indicated_schemas` does not include inplace applicator schemas, such as the
177
+ # subschemas of `allOf`, whereas `#jsi_schemas` does.
178
+ #
179
+ # this does include indicated schemas which do not apply themselves, such as `$ref`
180
+ # schemas (on json schema drafts up to 7) - these are not included on `#jsi_schemas`.
181
+ #
182
+ # @return [JSI::SchemaSet]
183
+ attr_reader :jsi_indicated_schemas
184
+
185
+ # yields a JSI of each node at or below this one in this JSI's document.
186
+ #
187
+ # @param propertyNames [Boolean] Whether to also yield each object property
188
+ # name (Hash key) of any descendent which is a hash/object.
189
+ # These are described by `propertyNames` subshemas of that object's schemas.
190
+ # They are not actual descendents of this node.
191
+ # See {HashNode#jsi_each_propertyName}.
192
+ # @yield [JSI::Base] each descendent node, starting with self
193
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
194
+ def jsi_each_descendent_node(propertyNames: false, &block)
195
+ return to_enum(__method__, propertyNames: propertyNames) unless block
196
+
197
+ yield self
198
+
199
+ if propertyNames && is_a?(HashNode)
200
+ jsi_each_propertyName do |propertyName|
201
+ propertyName.jsi_each_descendent_node(propertyNames: propertyNames, &block)
202
+ end
203
+ end
204
+
205
+ jsi_each_child_token do |token|
206
+ jsi_child(token, as_jsi: true).jsi_each_descendent_node(propertyNames: propertyNames, &block)
207
+ end
208
+
209
+ nil
210
+ end
211
+
212
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
213
+ # descendent nodes for which the given block had a true-ish result.
214
+ #
215
+ # this method yields a node before recursively descending to its child nodes, so leaf nodes are yielded
216
+ # last, after their parents. if a node is not selected, its descendents are never recursed.
217
+ #
218
+ # @yield [JSI::Base] each descendent node below self
219
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
220
+ def jsi_select_descendents_node_first(&block)
221
+ jsi_modified_copy do |instance|
222
+ if jsi_array? || jsi_hash?
223
+ res = instance.class.new
224
+ jsi_each_child_token do |token|
225
+ v = jsi_child(token, as_jsi: true)
226
+ if yield(v)
227
+ res_v = v.jsi_select_descendents_node_first(&block).jsi_node_content
228
+ if jsi_array?
229
+ res << res_v
230
+ else
231
+ res[token] = res_v
232
+ end
233
+ end
234
+ end
235
+ res
236
+ else
237
+ instance
238
+ end
239
+ end
240
+ end
241
+
242
+ # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
243
+ # descendent nodes for which the given block had a true-ish result.
244
+ #
245
+ # this method recursively descends child nodes before yielding each node, so leaf nodes are yielded
246
+ # before their parents.
247
+ #
248
+ # @yield [JSI::Base] each descendent node below self
249
+ # @return [JSI::Base] modified copy of self containing only the selected nodes
250
+ def jsi_select_descendents_leaf_first(&block)
251
+ jsi_modified_copy do |instance|
252
+ if jsi_array? || jsi_hash?
253
+ res = instance.class.new
254
+ jsi_each_child_token do |token|
255
+ v = jsi_child(token, as_jsi: true).jsi_select_descendents_leaf_first(&block)
256
+ if yield(v)
257
+ res_v = v.jsi_node_content
258
+ if jsi_array?
259
+ res << res_v
260
+ else
261
+ res[token] = res_v
262
+ end
263
+ end
264
+ end
265
+ res
266
+ else
267
+ instance
268
+ end
269
+ end
270
+ end
271
+
272
+ # an array of JSI instances above this one in the document.
273
+ #
274
+ # @return [Array<JSI::Base>]
275
+ def jsi_parent_nodes
276
+ parent = jsi_root_node
277
+
278
+ jsi_ptr.tokens.map do |token|
279
+ parent.tap do
280
+ parent = parent[token, as_jsi: true]
281
+ end
282
+ end.reverse!.freeze
283
+ end
284
+
285
+ # the immediate parent of this JSI. nil if there is no parent.
286
+ #
287
+ # @return [JSI::Base, nil]
288
+ def jsi_parent_node
289
+ jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
290
+ end
291
+
292
+ # ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
293
+ #
294
+ # @return [Array<JSI::Base>]
295
+ def jsi_ancestor_nodes
296
+ ancestors = []
297
+ ancestor = jsi_root_node
298
+ ancestors << ancestor
299
+
300
+ jsi_ptr.tokens.each do |token|
301
+ ancestor = ancestor[token, as_jsi: true]
302
+ ancestors << ancestor
303
+ end
304
+ ancestors.reverse!.freeze
305
+ end
306
+
307
+ # the descendent node at the given pointer
308
+ #
309
+ # @param ptr [JSI::Ptr, #to_ary]
310
+ # @return [JSI::Base]
311
+ def jsi_descendent_node(ptr)
312
+ descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
313
+ descendent
314
+ end
315
+
316
+ # A shorthand alias for {#jsi_descendent_node}.
317
+ #
318
+ # Note that, though more convenient to type, using an operator whose meaning may not be intuitive
319
+ # to a reader could impair readability of code.
320
+ #
321
+ # examples:
322
+ #
323
+ # my_jsi / ['foo', 'bar']
324
+ # my_jsi / %w(foo bar)
325
+ # my_schema / JSI::Ptr['additionalProperties']
326
+ # my_schema / %w(properties foo items additionalProperties)
327
+ #
328
+ # @param (see #jsi_descendent_node)
329
+ # @return (see #jsi_descendent_node)
330
+ def /(ptr)
331
+ jsi_descendent_node(ptr)
332
+ end
333
+
334
+ # yields each token (array index or hash key) identifying a child node.
335
+ # yields nothing if this node is not complex or has no children.
336
+ #
337
+ # @yield [String, Integer] each child token
338
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
339
+ def jsi_each_child_token
340
+ # note: overridden by Base::HashNode, Base::ArrayNode
341
+ return to_enum(__method__) { 0 } unless block_given?
342
+ nil
343
+ end
344
+
345
+ # Does the given token identify a child of this node?
346
+ #
347
+ # In other words, is the given token an array index or hash key of the instance?
348
+ #
349
+ # Always false if this is not a complex node.
350
+ #
351
+ # @param token [String, Integer]
352
+ # @return [Boolean]
353
+ def jsi_child_token_in_range?(token)
354
+ # note: overridden by Base::HashNode, Base::ArrayNode
355
+ false
356
+ end
357
+
358
+ # The child of the {#jsi_node_content} identified by the given token,
359
+ # or `nil` if the token does not identify an existing child.
360
+ #
361
+ # In other words, the element of the instance array at the given index,
362
+ # or the value of the instance hash/object for the given key.
363
+ #
364
+ # @return [Object, nil]
365
+ # @raise [SimpleNodeChildError] if this node is not complex (its instance is not array or hash)
366
+ def jsi_node_content_child(token)
367
+ # note: overridden by Base::HashNode, Base::ArrayNode
368
+ jsi_simple_node_child_error(token)
369
+ end
370
+
371
+ # A child JSI node, or the child of our {#jsi_instance}, identified by the given token.
372
+ # The token must identify an existing child; behavior if the child does not exist is undefined.
373
+ #
374
+ # @param token (see Base#[])
375
+ # @param as_jsi (see Base#[])
376
+ # @return [JSI::Base, Object]
377
+ def jsi_child(token, as_jsi: )
378
+ child_content = jsi_node_content_child(token)
379
+
380
+ child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
381
+ child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
382
+
383
+ jsi_child_as_jsi(child_content, child_applied_schemas, as_jsi) do
384
+ @child_node_map[
385
+ token: token,
386
+ child_indicated_schemas: child_indicated_schemas,
387
+ child_applied_schemas: child_applied_schemas,
388
+ includes: SchemaClasses.includes_for(child_content),
389
+ ]
390
+ end
391
+ end
392
+ private :jsi_child # internals for #[] but idk, could be public
393
+
394
+ # A default value for a child of this node identified by the given token, if schemas describing
395
+ # that child define a default value.
396
+ #
397
+ # If no schema describes a default value for the child (or in the unusual case that multiple
398
+ # schemas define different defaults), the result is `nil`.
399
+ #
400
+ # See also the `use_default` param of {Base#[]}.
401
+ #
402
+ # @param token (see Base#[])
403
+ # @param as_jsi (see Base#[])
404
+ # @return [JSI::Base, nil]
405
+ def jsi_default_child(token, as_jsi: )
406
+ child_content = jsi_node_content_child(token)
407
+
408
+ child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
409
+ child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
410
+
411
+ defaults = Set.new
412
+ child_applied_schemas.each do |child_schema|
413
+ if child_schema.keyword?('default')
414
+ defaults << child_schema.jsi_node_content['default']
415
+ end
416
+ end
417
+
418
+ if defaults.size == 1
419
+ # use the default value
420
+ jsi_child_as_jsi(defaults.first, child_applied_schemas, as_jsi) do
421
+ jsi_modified_copy do |i|
422
+ i.dup.tap { |i_dup| i_dup[token] = defaults.first }
423
+ end[token, as_jsi: true]
424
+ end
425
+ else
426
+ child_content
427
+ end
428
+ end
429
+ private :jsi_default_child # internals for #[] but idk, could be public
430
+
431
+ # subscripts to return a child value identified by the given token.
432
+ #
433
+ # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
434
+ # of the instance identifying the child value
435
+ # @param as_jsi [:auto, true, false] (default is `:auto`)
436
+ # Whether to return the child as a JSI. One of:
437
+ #
438
+ # - `:auto`: By default a JSI will be returned when either:
439
+ #
440
+ # - the result is a complex value (responds to #to_ary or #to_hash)
441
+ # - the result is a schema (including true/false schemas)
442
+ #
443
+ # The plain content is returned when it is a simple type.
444
+ #
445
+ # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
446
+ # empty if no schemas describe the instance.
447
+ # - false: the result value will always be the plain instance.
448
+ #
449
+ # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
450
+ # is not a hash key or array index of the instance and no default value applies.
451
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
452
+ # unspecified behavior.)
453
+ # @param use_default [true, false] (default is `false`)
454
+ # Whether to return a schema default value when the token refers to a child that is not in the document.
455
+ # If the token is not an array index or hash key of the instance, and one schema for the child
456
+ # instance specifies a default value, that default is returned.
457
+ #
458
+ # if the result with the default value is a JSI (per the `as_jsi` param), that JSI is not a child of
459
+ # this JSI - this JSI is not modified to fill in the default value. the result is a JSI within a new
460
+ # document containing the filled-in default.
461
+ #
462
+ # if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
463
+ # defaults are specified across those schemas), nil is returned.
464
+ # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
465
+ # unspecified behavior.)
466
+ # @return [JSI::Base, Object, nil] the child value identified by the subscript token
467
+ def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
468
+ # note: overridden by Base::HashNode, Base::ArrayNode
469
+ jsi_simple_node_child_error(token)
470
+ end
471
+
472
+ # The default value for the param `as_jsi` of {#[]}, controlling whether a child is returned as a JSI instance.
473
+ # @return [:auto, true, false] a valid value of the `as_jsi` param of {#[]}
474
+ def jsi_child_as_jsi_default
475
+ :auto
476
+ end
477
+
478
+ # The default value for the param `use_default` of {#[]}, controlling whether a schema default value is
479
+ # returned when a token refers to a child that is not in the document.
480
+ # @return [true, false] a valid value of the `use_default` param of {#[]}
481
+ def jsi_child_use_default_default
482
+ false
483
+ end
484
+
485
+ # assigns the subscript of the instance identified by the given token to the given value.
486
+ # if the value is a JSI, its instance is assigned instead of the JSI value itself.
487
+ #
488
+ # @param token [String, Integer, Object] token identifying the subscript to assign
489
+ # @param value [JSI::Base, Object] the value to be assigned
490
+ def []=(token, value)
491
+ unless jsi_array? || jsi_hash?
492
+ jsi_simple_node_child_error(token)
493
+ end
494
+ if value.is_a?(Base)
495
+ self[token] = value.jsi_instance
496
+ else
497
+ jsi_instance[token] = value
498
+ end
499
+ end
500
+
501
+ # the set of JSI schema modules corresponding to the schemas that describe this JSI
502
+ # @return [Set<Module>]
503
+ def jsi_schema_modules
504
+ Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
505
+ end
506
+
507
+ # Is this JSI described by the given schema (or schema module)?
508
+ #
509
+ # @param schema [Schema, SchemaModule]
510
+ # @return [Boolean]
511
+ def described_by?(schema)
512
+ if schema.is_a?(Schema)
513
+ jsi_schemas.include?(schema)
514
+ elsif schema.is_a?(SchemaModule)
515
+ jsi_schema_modules.include?(schema)
516
+ else
517
+ raise(TypeError, "expected a Schema or Schema Module; got: #{schema.pretty_inspect.chomp}")
518
+ end
519
+ end
520
+
521
+ # yields the content of this JSI's instance. the block must result in
522
+ # a modified copy of the yielded instance (not modified in place, which would alter this JSI
523
+ # as well) which will be used to instantiate and return a new JSI with the modified content.
524
+ #
525
+ # the result may have different schemas which describe it than this JSI's schemas,
526
+ # if conditional applicator schemas apply differently to the modified instance.
527
+ #
528
+ # @yield [Object] this JSI's instance. the block should result
529
+ # in a nondestructively modified copy of this.
530
+ # @return [JSI::Base subclass] the modified copy of self
531
+ def jsi_modified_copy(&block)
532
+ modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
533
+ modified_jsi_root_node = @jsi_root_node.jsi_indicated_schemas.new_jsi(modified_document,
534
+ uri: @jsi_root_node.jsi_schema_base_uri,
535
+ register: false, # default is already false but this is a place to be explicit
536
+ schema_registry: jsi_schema_registry,
537
+ )
538
+ modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
539
+ end
540
+
541
+ # Is the instance an array?
542
+ #
543
+ # An array is typically an instance of the Array class but may be an object that supports
544
+ # [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
545
+ # with a `#to_ary` method.
546
+ #
547
+ # @return [Boolean]
548
+ def jsi_array?
549
+ # note: overridden by Base::ArrayNode
550
+ false
551
+ end
552
+
553
+ # Is the instance a ruby Hash (JSON object)?
554
+ #
555
+ # This is typically an instance of the Hash class but may be an object that supports
556
+ # [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
557
+ # with a `#to_hash` method.
558
+ #
559
+ # @return [Boolean]
560
+ def jsi_hash?
561
+ # note: overridden by Base::HashNode
562
+ false
563
+ end
564
+
565
+ # validates this JSI's instance against its schemas
566
+ #
567
+ # @return [JSI::Validation::FullResult]
568
+ def jsi_validate
569
+ jsi_indicated_schemas.instance_validate(self)
570
+ end
571
+
572
+ # whether this JSI's instance is valid against all of its schemas
573
+ # @return [Boolean]
574
+ def jsi_valid?
575
+ jsi_indicated_schemas.instance_valid?(self)
576
+ end
577
+
578
+ # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
579
+ # see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
580
+ #
581
+ # the JMESPath gem is not a dependency of JSI, so must be installed / added to your Gemfile to use.
582
+ # e.g. `gem 'jmespath', '~> 1.5'`. note that versions below 1.5 are not compatible with JSI.
583
+ #
584
+ # @param expression [String] a [JMESPath](https://jmespath.org/) expression
585
+ # @param runtime_options passed to [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method),
586
+ # though no runtime_options are publicly documented or normally used.
587
+ # @return [Array, Object, nil] query results.
588
+ # see [JMESPath.search](https://rubydoc.info/gems/jmespath/JMESPath#search-class_method)
589
+ def jmespath_search(expression, **runtime_options)
590
+ Util.require_jmespath
591
+
592
+ JMESPath.search(expression, self, **runtime_options)
593
+ end
594
+
595
+ def dup
596
+ jsi_modified_copy(&:dup)
597
+ end
598
+
599
+ # a string representing this JSI, indicating any named schemas and inspecting its instance
600
+ # @return [String]
601
+ def inspect
602
+ -"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
603
+ end
604
+
605
+ alias_method :to_s, :inspect
606
+
607
+ # pretty-prints a representation of this JSI to the given printer
608
+ # @return [void]
609
+ def pretty_print(q)
610
+ q.text '#<'
611
+ q.text jsi_object_group_text.join(' ')
612
+ q.group_sub {
613
+ q.nest(2) {
614
+ q.breakable ' '
615
+ q.pp jsi_instance
616
+ }
617
+ }
618
+ q.breakable ''
619
+ q.text '>'
620
+ end
621
+
622
+ # @private
623
+ # @return [Array<String>]
624
+ def jsi_object_group_text
625
+ schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
626
+ if schema_names.empty?
627
+ class_txt = "JSI"
628
+ else
629
+ class_txt = -"JSI (#{schema_names.join(', ')})"
630
+ end
631
+
632
+ if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
633
+ if jsi_node_content.respond_to?(:jsi_object_group_text)
634
+ content_txt = jsi_node_content.jsi_object_group_text
635
+ else
636
+ content_txt = jsi_node_content.class.to_s
637
+ end
638
+ else
639
+ content_txt = nil
640
+ end
641
+
642
+ [
643
+ class_txt,
644
+ is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
645
+ *content_txt,
646
+ ].compact
647
+ end
648
+
649
+ # A structure coerced to JSONifiable types from the instance content.
650
+ # Calls {Util.as_json} with the instance and any given options.
651
+ def as_json(options = {})
652
+ Util.as_json(jsi_instance, **options)
653
+ end
654
+
655
+ # A JSON encoded string of the instance content.
656
+ # Calls {Util.to_json} with the instance and any given options.
657
+ # @return [String]
658
+ def to_json(options = {})
659
+ Util.to_json(jsi_instance, **options)
660
+ end
661
+
662
+ # see {Util::Private::FingerprintHash}
663
+ # @api private
664
+ def jsi_fingerprint
665
+ {
666
+ class: jsi_class,
667
+ jsi_document: jsi_document,
668
+ jsi_ptr: jsi_ptr,
669
+ # for instances in documents with schemas:
670
+ jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
671
+ # different registries mean references may resolve to different resources so must not be equal
672
+ jsi_schema_registry: jsi_schema_registry,
673
+ }
674
+ end
675
+ include Util::FingerprintHash
676
+
677
+ private
678
+
679
+ def jsi_memomaps_initialize
680
+ @child_indicated_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_indicated_schemas_compute))
681
+ @child_applied_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_applied_schemas_compute))
682
+ @child_node_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_node_compute))
683
+ end
684
+
685
+ def jsi_indicated_schemas=(jsi_indicated_schemas)
686
+ #chkbug raise(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
687
+ @jsi_indicated_schemas = jsi_indicated_schemas
688
+ end
689
+
690
+ def jsi_child_node_compute(token: , child_indicated_schemas: , child_applied_schemas: , includes: )
691
+ jsi_class = JSI::SchemaClasses.class_for_schemas(child_applied_schemas,
692
+ includes: includes,
693
+ )
694
+ jsi_class.new(@jsi_document,
695
+ jsi_ptr: @jsi_ptr[token],
696
+ jsi_indicated_schemas: child_indicated_schemas,
697
+ jsi_schema_base_uri: jsi_resource_ancestor_uri,
698
+ jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
699
+ jsi_schema_registry: jsi_schema_registry,
700
+ jsi_root_node: @jsi_root_node,
701
+ )
702
+ end
703
+
704
+ def jsi_child_indicated_schemas_compute(token: , content: )
705
+ jsi_schemas.child_applicator_schemas(token, content)
706
+ end
707
+
708
+ def jsi_child_applied_schemas_compute(token: , child_indicated_schemas: , child_content: )
709
+ child_indicated_schemas.inplace_applicator_schemas(child_content)
710
+ end
711
+
712
+ def jsi_child_as_jsi(child_content, child_schemas, as_jsi)
713
+ if [true, false].include?(as_jsi)
714
+ child_as_jsi = as_jsi
715
+ elsif as_jsi == :auto
716
+ child_is_complex = child_content.respond_to?(:to_hash) || child_content.respond_to?(:to_ary)
717
+ child_is_schema = child_schemas.any?(&:describes_schema?)
718
+ child_as_jsi = child_is_complex || child_is_schema
719
+ else
720
+ raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
721
+ end
722
+
723
+ if child_as_jsi
724
+ yield
725
+ else
726
+ child_content
727
+ end
728
+ end
729
+
730
+ def jsi_simple_node_child_error(token)
731
+ raise(SimpleNodeChildError, [
732
+ "cannot access a child of this JSI node because this node is not complex",
733
+ "using token: #{token.inspect}",
734
+ "instance: #{jsi_instance.pretty_inspect.chomp}",
735
+ ].join("\n"))
736
+ end
737
+ end
738
+ end