jsi 0.0.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +3 -1
- data/CHANGELOG.md +48 -0
- data/LICENSE.md +613 -0
- data/README.md +84 -45
- data/jsi.gemspec +11 -14
- data/lib/jsi.rb +31 -12
- data/lib/jsi/base.rb +310 -344
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +91 -0
- data/lib/jsi/json-schema-fragments.rb +3 -135
- data/lib/jsi/json.rb +3 -0
- data/lib/jsi/json/node.rb +72 -197
- data/lib/jsi/json/pointer.rb +419 -0
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +218 -0
- data/lib/jsi/pathed_node.rb +118 -0
- data/lib/jsi/schema.rb +168 -223
- data/lib/jsi/schema_classes.rb +158 -0
- data/lib/jsi/simple_wrap.rb +12 -0
- data/lib/jsi/typelike_modules.rb +71 -45
- data/lib/jsi/util.rb +47 -57
- data/lib/jsi/version.rb +1 -1
- data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +210 -84
- data/test/base_hash_test.rb +201 -58
- data/test/base_test.rb +212 -121
- data/test/jsi_coder_test.rb +85 -0
- data/test/jsi_json_arraynode_test.rb +26 -25
- data/test/jsi_json_hashnode_test.rb +40 -39
- data/test/jsi_json_node_test.rb +95 -126
- data/test/jsi_json_pointer_test.rb +102 -0
- data/test/jsi_typelike_as_json_test.rb +53 -0
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +109 -97
- data/test/spreedly_openapi_test.rb +8 -0
- data/test/test_helper.rb +42 -8
- data/test/util_test.rb +14 -14
- metadata +54 -25
- data/LICENSE.txt +0 -21
- data/lib/jsi/schema_instance_json_coder.rb +0 -83
- data/lib/jsi/struct_json_coder.rb +0 -30
- data/test/schema_instance_json_coder_test.rb +0 -121
- 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,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
|