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.
- 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
|