jsi-dev 0.0.2

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