jsi 0.0.4 → 0.4.0

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