jsi 0.0.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +3 -1
  3. data/CHANGELOG.md +48 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +84 -45
  6. data/jsi.gemspec +11 -14
  7. data/lib/jsi.rb +31 -12
  8. data/lib/jsi/base.rb +310 -344
  9. data/lib/jsi/base/to_rb.rb +2 -0
  10. data/lib/jsi/jsi_coder.rb +91 -0
  11. data/lib/jsi/json-schema-fragments.rb +3 -135
  12. data/lib/jsi/json.rb +3 -0
  13. data/lib/jsi/json/node.rb +72 -197
  14. data/lib/jsi/json/pointer.rb +419 -0
  15. data/lib/jsi/metaschema.rb +7 -0
  16. data/lib/jsi/metaschema_node.rb +218 -0
  17. data/lib/jsi/pathed_node.rb +118 -0
  18. data/lib/jsi/schema.rb +168 -223
  19. data/lib/jsi/schema_classes.rb +158 -0
  20. data/lib/jsi/simple_wrap.rb +12 -0
  21. data/lib/jsi/typelike_modules.rb +71 -45
  22. data/lib/jsi/util.rb +47 -57
  23. data/lib/jsi/version.rb +1 -1
  24. data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
  25. data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
  26. data/resources/icons/AGPL-3.0.png +0 -0
  27. data/test/base_array_test.rb +210 -84
  28. data/test/base_hash_test.rb +201 -58
  29. data/test/base_test.rb +212 -121
  30. data/test/jsi_coder_test.rb +85 -0
  31. data/test/jsi_json_arraynode_test.rb +26 -25
  32. data/test/jsi_json_hashnode_test.rb +40 -39
  33. data/test/jsi_json_node_test.rb +95 -126
  34. data/test/jsi_json_pointer_test.rb +102 -0
  35. data/test/jsi_typelike_as_json_test.rb +53 -0
  36. data/test/metaschema_node_test.rb +19 -0
  37. data/test/schema_module_test.rb +21 -0
  38. data/test/schema_test.rb +109 -97
  39. data/test/spreedly_openapi_test.rb +8 -0
  40. data/test/test_helper.rb +42 -8
  41. data/test/util_test.rb +14 -14
  42. metadata +54 -25
  43. data/LICENSE.txt +0 -21
  44. data/lib/jsi/schema_instance_json_coder.rb +0 -83
  45. data/lib/jsi/struct_json_coder.rb +0 -30
  46. data/test/schema_instance_json_coder_test.rb +0 -121
  47. data/test/struct_json_coder_test.rb +0 -130
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module JSON
5
+ # a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
6
+ class Pointer
7
+ class Error < StandardError
8
+ end
9
+ class PointerSyntaxError < Error
10
+ end
11
+ class ReferenceError < Error
12
+ end
13
+
14
+ # instantiates a Pointer from any given reference tokens.
15
+ #
16
+ # >> JSI::JSON::Pointer[]
17
+ # => #<JSI::JSON::Pointer reference_tokens: []>
18
+ # >> JSI::JSON::Pointer['a', 'b']
19
+ # => #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>
20
+ # >> JSI::JSON::Pointer['a']['b']
21
+ # => #<JSI::JSON::Pointer reference_tokens: ["a", "b"]>
22
+ #
23
+ # note in the last example that you can conveniently chain the class .[] method
24
+ # with the instance #[] method.
25
+ #
26
+ # @param *reference_tokens any number of reference tokens
27
+ # @return [JSI::JSON::Pointer]
28
+ def self.[](*reference_tokens)
29
+ new(reference_tokens)
30
+ end
31
+
32
+ # parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer
33
+ #
34
+ # ptr = JSI::JSON::Pointer.from_fragment('/foo/bar')
35
+ # => #<JSI::JSON::Pointer fragment: /foo/bar>
36
+ # ptr.reference_tokens
37
+ # => ["foo", "bar"]
38
+ #
39
+ # with URI escaping:
40
+ #
41
+ # ptr = JSI::JSON::Pointer.from_fragment('/foo%20bar')
42
+ # => #<JSI::JSON::Pointer fragment: /foo%20bar>
43
+ # ptr.reference_tokens
44
+ # => ["foo bar"]
45
+ #
46
+ # @param fragment [String] a fragment containing a pointer (starting with #)
47
+ # @return [JSI::JSON::Pointer]
48
+ # @raise [JSI::JSON::Pointer::PointerSyntaxError] when the fragment does not contain a pointer with valid pointer syntax
49
+ def self.from_fragment(fragment)
50
+ from_pointer(Addressable::URI.unescape(fragment), type: 'fragment')
51
+ end
52
+
53
+ # parse a pointer string and instantiate as a JSI::JSON::Pointer
54
+ #
55
+ # ptr1 = JSI::JSON::Pointer.from_pointer('/foo')
56
+ # => #<JSI::JSON::Pointer pointer: /foo>
57
+ # ptr1.reference_tokens
58
+ # => ["foo"]
59
+ #
60
+ # ptr2 = JSI::JSON::Pointer.from_pointer('/foo~0bar/baz~1qux')
61
+ # => #<JSI::JSON::Pointer pointer: /foo~0bar/baz~1qux>
62
+ # ptr2.reference_tokens
63
+ # => ["foo~bar", "baz/qux"]
64
+ #
65
+ # @param pointer_string [String] a pointer string
66
+ # @param type (for internal use) indicates the original representation of the pointer
67
+ # @return [JSI::JSON::Pointer]
68
+ # @raise [JSI::JSON::Pointer::PointerSyntaxError] when the pointer_string does not have valid pointer syntax
69
+ def self.from_pointer(pointer_string, type: 'pointer')
70
+ tokens = pointer_string.split('/', -1).map! do |piece|
71
+ piece.gsub('~1', '/').gsub('~0', '~')
72
+ end
73
+ if tokens[0] == ''
74
+ new(tokens[1..-1], type: type)
75
+ elsif tokens.empty?
76
+ new(tokens, type: type)
77
+ else
78
+ raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
79
+ end
80
+ end
81
+
82
+ # initializes a JSI::JSON::Pointer from the given reference_tokens.
83
+ #
84
+ # @param reference_tokens [Array<Object>]
85
+ # @param type [String, Symbol] one of 'pointer' or 'fragment'
86
+ def initialize(reference_tokens, type: nil)
87
+ unless reference_tokens.respond_to?(:to_ary)
88
+ raise(TypeError, "reference_tokens must be an array. got: #{reference_tokens.inspect}")
89
+ end
90
+ @reference_tokens = reference_tokens.to_ary.map(&:freeze).freeze
91
+ @type = type.is_a?(Symbol) ? type.to_s : type
92
+ end
93
+
94
+ attr_reader :reference_tokens
95
+
96
+ # takes a root json document and evaluates this pointer through the document, returning the value
97
+ # pointed to by this pointer.
98
+ #
99
+ # @param document [#to_ary, #to_hash] the document against which we will evaluate this pointer
100
+ # @return [Object] the content of the document pointed to by this pointer
101
+ # @raise [JSI::JSON::Pointer::ReferenceError] the document does not contain the path this pointer references
102
+ def evaluate(document)
103
+ res = reference_tokens.inject(document) do |value, token|
104
+ if value.respond_to?(:to_ary)
105
+ if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
106
+ token = token.to_i
107
+ end
108
+ unless token.is_a?(Integer)
109
+ raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
110
+ end
111
+ unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
112
+ raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
113
+ end
114
+ (value.respond_to?(:[]) ? value : value.to_ary)[token]
115
+ elsif value.respond_to?(:to_hash)
116
+ unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
117
+ raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
118
+ end
119
+ (value.respond_to?(:[]) ? value : value.to_hash)[token]
120
+ else
121
+ raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
122
+ end
123
+ end
124
+ res
125
+ end
126
+
127
+ # @return [String] the pointer string representation of this Pointer
128
+ def pointer
129
+ reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
130
+ end
131
+
132
+ # @return [String] the fragment string representation of this Pointer
133
+ def fragment
134
+ Addressable::URI.escape(pointer)
135
+ end
136
+
137
+ # @return [Addressable::URI] a URI consisting only of a pointer fragment
138
+ def uri
139
+ Addressable::URI.new(fragment: fragment)
140
+ end
141
+
142
+ # @return [Boolean] whether this pointer points to the root (has an empty array of reference_tokens)
143
+ def root?
144
+ reference_tokens.empty?
145
+ end
146
+
147
+ # @return [JSI::JSON::Pointer] pointer to the parent of where this pointer points
148
+ # @raise [JSI::JSON::Pointer::ReferenceError] if this pointer has no parent (points to the root)
149
+ def parent
150
+ if root?
151
+ raise(ReferenceError, "cannot access parent of root pointer: #{pretty_inspect.chomp}")
152
+ else
153
+ Pointer.new(reference_tokens[0...-1], type: @type)
154
+ end
155
+ end
156
+
157
+ # @return [Boolean] does this pointer contain the other_ptr - that is, is this pointer an
158
+ # ancestor of other_ptr, a child pointer. contains? is inclusive; a pointer does contain itself.
159
+ def contains?(other_ptr)
160
+ self.reference_tokens == other_ptr.reference_tokens[0...self.reference_tokens.size]
161
+ end
162
+
163
+ # @return [JSI::JSON::Pointer] returns this pointer relative to the given ancestor_ptr
164
+ # @raise [JSI::JSON::Pointer::ReferenceError] if the given ancestor_ptr is not an ancestor of this pointer
165
+ def ptr_relative_to(ancestor_ptr)
166
+ unless ancestor_ptr.contains?(self)
167
+ raise(ReferenceError, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
168
+ end
169
+ Pointer.new(reference_tokens[ancestor_ptr.reference_tokens.size..-1], type: @type)
170
+ end
171
+
172
+ # @param ptr [JSI::JSON::Pointer]
173
+ # @return [JSI::JSON::Pointer] a pointer with the reference tokens of this one plus the given ptr's.
174
+ def +(ptr)
175
+ unless ptr.is_a?(JSI::JSON::Pointer)
176
+ raise(TypeError, "ptr must be a JSI::JSON::Pointer; got: #{ptr.inspect}")
177
+ end
178
+ Pointer.new(reference_tokens + ptr.reference_tokens, type: @type)
179
+ end
180
+
181
+ # @param n [Integer]
182
+ # @return [JSI::JSON::Pointer] a Pointer consisting of the first n of our reference_tokens
183
+ # @raise [ArgumentError] if n is not between 0 and the size of our reference_tokens
184
+ def take(n)
185
+ unless (0..reference_tokens.size).include?(n)
186
+ raise(ArgumentError, "n not in range (0..#{reference_tokens.size}): #{n.inspect}")
187
+ end
188
+ Pointer.new(reference_tokens.take(n), type: @type)
189
+ end
190
+
191
+ # appends the given token to this Pointer's reference tokens and returns the result
192
+ #
193
+ # @param token [Object]
194
+ # @return [JSI::JSON::Pointer] pointer to a child node of this pointer with the given token
195
+ def [](token)
196
+ Pointer.new(reference_tokens + [token], type: @type)
197
+ end
198
+
199
+ # given this Pointer points to a schema in the given document, returns a set of pointers
200
+ # to subschemas of that schema for the given property name.
201
+ #
202
+ # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
203
+ # @param property_name [Object] the property name for which to find a subschema
204
+ # @return [Set<JSI::JSON::Pointer>] pointers to subschemas
205
+ def schema_subschema_ptrs_for_property_name(document, property_name)
206
+ ptr = self
207
+ schema = ptr.evaluate(document)
208
+ Set.new.tap do |ptrs|
209
+ if schema.respond_to?(:to_hash)
210
+ apply_additional = true
211
+ if schema.key?('properties') && schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(property_name)
212
+ apply_additional = false
213
+ ptrs << ptr['properties'][property_name]
214
+ end
215
+ if schema['patternProperties'].respond_to?(:to_hash)
216
+ schema['patternProperties'].each_key do |pattern|
217
+ if property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
218
+ apply_additional = false
219
+ ptrs << ptr['patternProperties'][pattern]
220
+ end
221
+ end
222
+ end
223
+ if apply_additional && schema.key?('additionalProperties')
224
+ ptrs << ptr['additionalProperties']
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ # given this Pointer points to a schema in the given document, returns a set of pointers
231
+ # to subschemas of that schema for the given array index.
232
+ #
233
+ # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
234
+ # @param idx [Object] the array index for which to find subschemas
235
+ # @return [Set<JSI::JSON::Pointer>] pointers to subschemas
236
+ def schema_subschema_ptrs_for_index(document, idx)
237
+ ptr = self
238
+ schema = ptr.evaluate(document)
239
+ Set.new.tap do |ptrs|
240
+ if schema.respond_to?(:to_hash)
241
+ if schema['items'].respond_to?(:to_ary)
242
+ if schema['items'].each_index.to_a.include?(idx)
243
+ ptrs << ptr['items'][idx]
244
+ elsif schema.key?('additionalItems')
245
+ ptrs << ptr['additionalItems']
246
+ end
247
+ elsif schema.key?('items')
248
+ ptrs << ptr['items']
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ # given this Pointer points to a schema in the given document, this matches any
255
+ # applicators of the schema (oneOf, anyOf, allOf, $ref) which should be applied
256
+ # and returns them as a set of pointers.
257
+ #
258
+ # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
259
+ # @param instance [Object] the instance to check any applicators against
260
+ # @return [JSI::JSON::Pointer] either a pointer to a *Of subschema in the document,
261
+ # or self if no other subschema was matched
262
+ def schema_match_ptrs_to_instance(document, instance)
263
+ ptr = self
264
+ schema = ptr.evaluate(document)
265
+
266
+ Set.new.tap do |ptrs|
267
+ if schema.respond_to?(:to_hash)
268
+ if schema['$ref'].respond_to?(:to_str)
269
+ ptr.deref(document) do |deref_ptr|
270
+ ptrs.merge(deref_ptr.schema_match_ptrs_to_instance(document, instance))
271
+ end
272
+ else
273
+ ptrs << ptr
274
+ end
275
+ if schema['allOf'].respond_to?(:to_ary)
276
+ schema['allOf'].each_index do |i|
277
+ ptrs.merge(ptr['allOf'][i].schema_match_ptrs_to_instance(document, instance))
278
+ end
279
+ end
280
+ if schema['anyOf'].respond_to?(:to_ary)
281
+ schema['anyOf'].each_index do |i|
282
+ valid = ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['anyOf'][i].fragment)
283
+ if valid
284
+ ptrs.merge(ptr['anyOf'][i].schema_match_ptrs_to_instance(document, instance))
285
+ end
286
+ end
287
+ end
288
+ if schema['oneOf'].respond_to?(:to_ary)
289
+ one_i = schema['oneOf'].each_index.detect do |i|
290
+ ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['oneOf'][i].fragment)
291
+ end
292
+ if one_i
293
+ ptrs.merge(ptr['oneOf'][one_i].schema_match_ptrs_to_instance(document, instance))
294
+ end
295
+ end
296
+ # TODO dependencies
297
+ else
298
+ ptrs << ptr
299
+ end
300
+ end
301
+ end
302
+
303
+ # takes a document and a block. the block is yielded the content of the given document at this
304
+ # pointer's location. the block must result a modified copy of that content (and MUST NOT modify
305
+ # the object it is given). this modified copy of that content is incorporated into a modified copy
306
+ # of the given document, which is then returned. the structure and contents of the document outside
307
+ # the path pointed to by this pointer is not modified.
308
+ #
309
+ # @param document [Object] the document to apply this pointer to
310
+ # @yield [Object] the content this pointer applies to in the given document
311
+ # the block must result in the new content which will be placed in the modified document copy.
312
+ # @return [Object] a copy of the given document, with the content this pointer applies to
313
+ # replaced by the result of the block
314
+ def modified_document_copy(document, &block)
315
+ # we need to preserve the rest of the document, but modify the content at our path.
316
+ #
317
+ # this is actually a bit tricky. we can't modify the original document, obviously.
318
+ # we could do a deep copy, but that's expensive. instead, we make a copy of each array
319
+ # or hash in the path above this node. this node's content is modified by the caller, and
320
+ # that is recursively merged up to the document root. the recursion is done with a
321
+ # y combinator, for no other reason than that was a fun way to implement it.
322
+ modified_document = JSI::Util.ycomb do |rec|
323
+ proc do |subdocument, subpath|
324
+ if subpath == []
325
+ Typelike.modified_copy(subdocument, &block)
326
+ else
327
+ car = subpath[0]
328
+ cdr = subpath[1..-1]
329
+ if subdocument.respond_to?(:to_hash)
330
+ subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
331
+ car_object = rec.call(subdocument_car, cdr)
332
+ if car_object.object_id == subdocument_car.object_id
333
+ subdocument
334
+ else
335
+ (subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
336
+ end
337
+ elsif subdocument.respond_to?(:to_ary)
338
+ if car.is_a?(String) && car =~ /\A\d+\z/
339
+ car = car.to_i
340
+ end
341
+ unless car.is_a?(Integer)
342
+ raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
343
+ end
344
+ subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
345
+ car_object = rec.call(subdocument_car, cdr)
346
+ if car_object.object_id == subdocument_car.object_id
347
+ subdocument
348
+ else
349
+ (subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
350
+ arr[car] = car_object
351
+ end
352
+ end
353
+ else
354
+ raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
355
+ end
356
+ end
357
+ end
358
+ end.call(document, reference_tokens)
359
+ modified_document
360
+ end
361
+
362
+ # if this Pointer points at a $ref node within the given document, #deref attempts
363
+ # to follow that $ref and return a Pointer to the referenced location. otherwise,
364
+ # this Pointer is returned.
365
+ #
366
+ # if the content this Pointer points to in the document is not hash-like, does not
367
+ # have a $ref property, its $ref cannot be found, or its $ref points outside the document,
368
+ # this pointer is returned.
369
+ #
370
+ # @param document [Object] the document this pointer applies to
371
+ # @yield [Pointer] if a block is given (optional), this will yield a deref'd pointer. if this
372
+ # pointer does not point to a $ref object in the given document, the block is not called.
373
+ # if we point to a $ref which cannot be followed (e.g. a $ref to an external
374
+ # document, which is not yet supported), the block is not called.
375
+ # @return [Pointer] dereferenced pointer, or this pointer
376
+ def deref(document, &block)
377
+ block ||= Util::NOOP
378
+ content = evaluate(document)
379
+
380
+ if content.respond_to?(:to_hash)
381
+ ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
382
+ end
383
+ return self unless ref.is_a?(String)
384
+
385
+ if ref[/\A#/]
386
+ return Pointer.from_fragment(Addressable::URI.parse(ref).fragment).tap(&block)
387
+ end
388
+
389
+ # HAX for how google does refs and ids
390
+ if document['schemas'].respond_to?(:to_hash)
391
+ if document['schemas'][ref]
392
+ return Pointer.new(['schemas', ref], type: 'hax').tap(&block)
393
+ end
394
+ document['schemas'].each do |k, schema|
395
+ if schema['id'] == ref
396
+ return Pointer.new(['schemas', k], type: 'hax').tap(&block)
397
+ end
398
+ end
399
+ end
400
+
401
+ #raise(NotImplementedError, "cannot dereference #{ref}") # TODO
402
+ return self
403
+ end
404
+
405
+ # @return [String] string representation of this Pointer
406
+ def inspect
407
+ "#{self.class.name}[#{reference_tokens.map(&:inspect).join(", ")}]"
408
+ end
409
+
410
+ alias_method :to_s, :inspect
411
+
412
+ # pointers are equal if the reference_tokens are equal, regardless of @type
413
+ def jsi_fingerprint
414
+ {class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
415
+ end
416
+ include Util::FingerprintHash
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Metaschema
5
+ include JSI::Schema::DescribesSchema
6
+ end
7
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # a MetaschemaNode is a PathedNode whose node_document contains a metaschema.
5
+ # as with any PathedNode the node_ptr points to the content of a node.
6
+ # the root of the metaschema is pointed to by metaschema_root_ptr.
7
+ # the schema of the root of the document is pointed to by root_schema_ptr.
8
+ #
9
+ # like JSI::Base, this class represents an instance of a schema, an instance
10
+ # which may itself be a schema. unlike JSI::Base, the document containing the
11
+ # schema and the instance is the same, and a schema may be an instance of itself.
12
+ #
13
+ # the document containing the metaschema, its subschemas, and instances of those
14
+ # subschemas is the node_document.
15
+ #
16
+ # the schema instance is the content in the document pointed to by the MetaschemaNode's node_ptr.
17
+ #
18
+ # unlike with JSI::Base, the schema is not part of the class, since a metaschema
19
+ # needs the ability to have its schema be the instance itself.
20
+ #
21
+ # if the MetaschemaNode's schema is its self, it will be extended with JSI::Metaschema.
22
+ #
23
+ # a MetaschemaNode is extended with JSI::Schema when it represents a schema - this is the case when
24
+ # its schema is the metaschema.
25
+ class MetaschemaNode
26
+ include PathedNode
27
+ include Util::Memoize
28
+
29
+ # not every MetaschemaNode is actually an Enumerable, but it's better to include Enumerable on
30
+ # the class than to conditionally extend the instance.
31
+ include Enumerable
32
+
33
+ # @param node_document the document containing the metaschema
34
+ # @param node_ptr [JSI::JSON::Pointer] ptr to this MetaschemaNode in node_document
35
+ # @param metaschema_root_ptr [JSI::JSON::Pointer] ptr to the root of the metaschema in node_document
36
+ # @param root_schema_ptr [JSI::JSON::Pointer] ptr to the schema of the root of the node_document
37
+ def initialize(node_document, node_ptr: JSI::JSON::Pointer[], metaschema_root_ptr: JSI::JSON::Pointer[], root_schema_ptr: JSI::JSON::Pointer[])
38
+ @node_document = node_document
39
+ @node_ptr = node_ptr
40
+ @metaschema_root_ptr = metaschema_root_ptr
41
+ @root_schema_ptr = root_schema_ptr
42
+
43
+ node_content = self.node_content
44
+
45
+ if node_content.respond_to?(:to_hash)
46
+ extend PathedHashNode
47
+ elsif node_content.respond_to?(:to_ary)
48
+ extend PathedArrayNode
49
+ end
50
+
51
+ instance_for_schema = node_document
52
+ schema_ptrs = node_ptr.reference_tokens.inject(Set.new << root_schema_ptr) do |ptrs, tok|
53
+ if instance_for_schema.respond_to?(:to_ary)
54
+ subschema_ptrs_for_token = ptrs.map do |ptr|
55
+ ptr.schema_subschema_ptrs_for_index(node_document, tok)
56
+ end.inject(Set.new, &:|)
57
+ else
58
+ subschema_ptrs_for_token = ptrs.map do |ptr|
59
+ ptr.schema_subschema_ptrs_for_property_name(node_document, tok)
60
+ end.inject(Set.new, &:|)
61
+ end
62
+ instance_for_schema = instance_for_schema[tok]
63
+ ptrs_for_instance = subschema_ptrs_for_token.map do |ptr|
64
+ ptr.schema_match_ptrs_to_instance(node_document, instance_for_schema)
65
+ end.inject(Set.new, &:|)
66
+ ptrs_for_instance
67
+ end
68
+
69
+ @jsi_schemas = schema_ptrs.map do |schema_ptr|
70
+ if schema_ptr == node_ptr
71
+ self
72
+ else
73
+ new_node(node_ptr: schema_ptr)
74
+ end
75
+ end.to_set
76
+
77
+ @jsi_schemas.each do |schema|
78
+ if schema.node_ptr == metaschema_root_ptr
79
+ extend JSI::Schema
80
+ end
81
+ if schema.node_ptr == node_ptr
82
+ extend Metaschema
83
+ end
84
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [Metaschema, Schema, MetaschemaNode, PathedArrayNode, PathedHashNode]))
85
+ end
86
+
87
+ # workarounds
88
+ begin # draft 4 boolean schema workaround
89
+ # in draft 4, boolean schemas are not described in the root, but on anyOf schemas on
90
+ # properties/additionalProperties and properties/additionalItems.
91
+ # we need to extend those as DescribesSchema.
92
+ addtlPropsanyOf = metaschema_root_ptr["properties"]["additionalProperties"]["anyOf"]
93
+ addtlItemsanyOf = metaschema_root_ptr["properties"]["additionalItems"]["anyOf"]
94
+
95
+ if !node_ptr.root? && [addtlPropsanyOf, addtlItemsanyOf].include?(node_ptr.parent)
96
+ extend JSI::Schema::DescribesSchema
97
+ end
98
+ end
99
+ end
100
+
101
+ # document containing the metaschema. see PathedNode#node_document.
102
+ attr_reader :node_document
103
+ # ptr to this metaschema node. see PathedNode#node_ptr.
104
+ attr_reader :node_ptr
105
+ # ptr to the root of the metaschema in the node_document
106
+ attr_reader :metaschema_root_ptr
107
+ # ptr to the schema of the root of the node_document
108
+ attr_reader :root_schema_ptr
109
+ # JSI::Schemas describing this MetaschemaNode
110
+ attr_reader :jsi_schemas
111
+
112
+ # @return [MetaschemaNode] document root MetaschemaNode
113
+ def document_root_node
114
+ new_node(node_ptr: JSI::JSON::Pointer[])
115
+ end
116
+
117
+ # @return [MetaschemaNode] parent MetaschemaNode
118
+ def parent_node
119
+ new_node(node_ptr: node_ptr.parent)
120
+ end
121
+
122
+ # @param token [String, Integer, Object] the token to subscript
123
+ # @return [MetaschemaNode, Object] the node content's subscript value at the given token.
124
+ # if there is a subschema defined for that token on this MetaschemaNode's schema,
125
+ # returns that value as a MetaschemaNode instantiation of that subschema.
126
+ def [](token)
127
+ if respond_to?(:to_hash)
128
+ token_in_range = node_content_hash_pubsend(:key?, token)
129
+ value = node_content_hash_pubsend(:[], token)
130
+ elsif respond_to?(:to_ary)
131
+ token_in_range = node_content_ary_pubsend(:each_index).include?(token)
132
+ value = node_content_ary_pubsend(:[], token)
133
+ else
134
+ raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from content: #{node_content.pretty_inspect.chomp}")
135
+ end
136
+
137
+ result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
138
+ if token_in_range
139
+ value_node = new_node(node_ptr: node_ptr[token])
140
+
141
+ if value_node.is_a?(Schema) || value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
142
+ value_node
143
+ else
144
+ value
145
+ end
146
+ else
147
+ # I think I will not support Hash#default/#default_proc in this case.
148
+ nil
149
+ end
150
+ end
151
+ result
152
+ end
153
+
154
+ # if this MetaschemaNode is a $ref then the $ref is followed. otherwise this MetaschemaNode is returned.
155
+ # @return [MetaschemaNode]
156
+ def deref(&block)
157
+ node_ptr_deref do |deref_ptr|
158
+ return new_node(node_ptr: deref_ptr).tap(&(block || Util::NOOP))
159
+ end
160
+ return self
161
+ end
162
+
163
+ # @yield [Object] the node content of the instance. the block should result
164
+ # in a (nondestructively) modified copy of this.
165
+ # @return [MetaschemaNode] modified copy of self
166
+ def modified_copy(&block)
167
+ MetaschemaNode.new(node_ptr.modified_document_copy(node_document, &block), our_initialize_params)
168
+ end
169
+
170
+ # @return [String]
171
+ def inspect
172
+ "\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
173
+ end
174
+
175
+ def pretty_print(q)
176
+ q.text '#<'
177
+ q.text object_group_text.join(' ')
178
+ q.group_sub {
179
+ q.nest(2) {
180
+ q.breakable ' '
181
+ q.pp node_content
182
+ }
183
+ }
184
+ q.breakable ''
185
+ q.text '>'
186
+ end
187
+
188
+ # @return [Array<String>]
189
+ def object_group_text
190
+ if jsi_schemas.any?
191
+ class_n_schemas = "#{self.class} (#{jsi_schemas.map { |s| s.node_ptr.uri }.join(' ')})"
192
+ else
193
+ class_n_schemas = self.class.to_s
194
+ end
195
+ [
196
+ class_n_schemas,
197
+ is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
198
+ *(node_content.respond_to?(:object_group_text) ? node_content.object_group_text : []),
199
+ ].compact
200
+ end
201
+
202
+ # @return [Object] an opaque fingerprint of this MetaschemaNode for FingerprintHash
203
+ def jsi_fingerprint
204
+ {class: self.class, node_document: node_document}.merge(our_initialize_params)
205
+ end
206
+ include Util::FingerprintHash
207
+
208
+ private
209
+
210
+ def our_initialize_params
211
+ {node_ptr: node_ptr, metaschema_root_ptr: metaschema_root_ptr, root_schema_ptr: root_schema_ptr}
212
+ end
213
+
214
+ def new_node(params)
215
+ MetaschemaNode.new(node_document, our_initialize_params.merge(params))
216
+ end
217
+ end
218
+ end