jsi 0.3.0 → 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 +7 -0
- data/LICENSE.md +1 -1
- data/README.md +1 -1
- data/lib/jsi.rb +13 -7
- data/lib/jsi/base.rb +84 -69
- data/lib/jsi/jsi_coder.rb +2 -2
- data/lib/jsi/json/node.rb +2 -2
- data/lib/jsi/json/pointer.rb +82 -90
- data/lib/jsi/metaschema_node.rb +27 -26
- data/lib/jsi/schema.rb +44 -61
- data/lib/jsi/schema_classes.rb +23 -16
- data/lib/jsi/util.rb +30 -29
- data/lib/jsi/version.rb +1 -1
- data/test/base_array_test.rb +14 -14
- data/test/base_hash_test.rb +13 -13
- data/test/base_test.rb +17 -22
- data/test/jsi_coder_test.rb +4 -4
- data/test/jsi_json_node_test.rb +4 -4
- data/test/jsi_json_pointer_test.rb +3 -7
- data/test/metaschema_node_test.rb +1 -1
- data/test/schema_test.rb +70 -48
- data/test/test_helper.rb +2 -2
- data/test/util_test.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42c370e01587ddef37138ffb1aacc4cd6240de94c0bbd9d61e96cafcb6ba829d
|
4
|
+
data.tar.gz: 9e26e8e2e8d78a05018302beff7190f33e0243d4f6899f5c3f1d046ccaaa3869
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75c5220df82e30b9332fba96d10463ee07cc52ff60cf8dc44963dd70c2472d279180659ab782098b5bcc9066dd000e07f60b1e7fa616215bfe6e6bfa11eddcb4
|
7
|
+
data.tar.gz: 848ac231235bebf262502fb7966265ab6c5e477bd6bb4acab038793dd5727a357f70482a3092e161cecc76eff4b126d4cdd7841965747fc2429ee76b5121128c
|
data/.simplecov
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
# v0.4.0
|
2
|
+
|
3
|
+
- a JSI::Base has multiple jsi_schemas https://github.com/notEthan/jsi/pull/88
|
4
|
+
- JSI.class_for_schemas replaces JSI.class_for_schema
|
5
|
+
- fix uri/fragment nomenclature https://github.com/notEthan/jsi/pull/89
|
6
|
+
|
1
7
|
# v0.3.0
|
2
8
|
|
3
9
|
- a schema is a JSI instance of a metaschema
|
@@ -5,6 +11,7 @@
|
|
5
11
|
- module JSI::Metaschema
|
6
12
|
- class JSI::MetaschemaNode
|
7
13
|
- JSI::JSON::Node breaking changes
|
14
|
+
- module SimpleWrap https://github.com/notEthan/jsi/pull/87
|
8
15
|
|
9
16
|
# v0.2.1
|
10
17
|
|
data/LICENSE.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copright © [Ethan](https://github.com/notEthan/)
|
1
|
+
Copright © [Ethan](https://github.com/notEthan/) <ethan.jsi@unth.net>
|
2
2
|
|
3
3
|
[<img align="right" src="https://github.com/notEthan/jsi/raw/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
|
4
4
|
|
data/README.md
CHANGED
@@ -226,6 +226,6 @@ Issues and pull requests are welcome on GitHub at https://github.com/notEthan/js
|
|
226
226
|
|
227
227
|
[<img align="right" src="https://github.com/notEthan/jsi/raw/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
|
228
228
|
|
229
|
-
JSI is
|
229
|
+
JSI is licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
|
230
230
|
|
231
231
|
Unlike the MIT or BSD licenses more commonly used with Ruby gems, this license requires that if you modify JSI and propagate your changes, e.g. by including it in a web application, your modified version must be publicly available. The common path of forking on Github should satisfy this requirement.
|
data/lib/jsi.rb
CHANGED
@@ -3,13 +3,21 @@
|
|
3
3
|
require "jsi/version"
|
4
4
|
require "pp"
|
5
5
|
require "set"
|
6
|
+
require "json"
|
6
7
|
require "pathname"
|
8
|
+
require "addressable/uri"
|
9
|
+
|
7
10
|
require "jsi/json-schema-fragments"
|
11
|
+
|
8
12
|
require "jsi/util"
|
13
|
+
require "jsi/typelike_modules"
|
9
14
|
|
10
15
|
module JSI
|
11
16
|
# generally put in code paths that are not expected to be valid control flow paths.
|
12
17
|
# rather a NotImplementedCorrectlyError. but that's too long.
|
18
|
+
#
|
19
|
+
# if you've found this class because JSI has raised this error, please open an issue with the backtrace
|
20
|
+
# and any context you can provide at https://github.com/notEthan/jsi/issues
|
13
21
|
class Bug < NotImplementedError
|
14
22
|
end
|
15
23
|
|
@@ -23,8 +31,6 @@ module JSI
|
|
23
31
|
autoload :Arraylike, 'jsi/typelike_modules'
|
24
32
|
autoload :Schema, 'jsi/schema'
|
25
33
|
autoload :Base, 'jsi/base'
|
26
|
-
autoload :BaseArray, 'jsi/base'
|
27
|
-
autoload :BaseHash, 'jsi/base'
|
28
34
|
autoload :Metaschema, 'jsi/metaschema'
|
29
35
|
autoload :MetaschemaNode, 'jsi/metaschema_node'
|
30
36
|
autoload :SchemaClasses, 'jsi/schema_classes'
|
@@ -35,10 +41,10 @@ module JSI
|
|
35
41
|
|
36
42
|
autoload :SimpleWrap, 'jsi/simple_wrap'
|
37
43
|
|
38
|
-
# @
|
39
|
-
#
|
40
|
-
#
|
41
|
-
def self.
|
42
|
-
SchemaClasses.
|
44
|
+
# @param schemas [Enumerable<JSI::Schema, #to_hash, Boolean>] schemas to represent with the class
|
45
|
+
# @return [Class subclassing JSI::Base] a JSI class which represents the given schemas.
|
46
|
+
# an instance of the class represents a JSON Schema instance described by all of the given schemas.
|
47
|
+
def self.class_for_schemas(*schemas)
|
48
|
+
SchemaClasses.class_for_schemas(*schemas)
|
43
49
|
end
|
44
50
|
end
|
data/lib/jsi/base.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'json'
|
4
|
-
require 'jsi/typelike_modules'
|
5
|
-
|
6
3
|
module JSI
|
7
4
|
# the base class for representing and instantiating a JSON Schema.
|
8
5
|
#
|
@@ -13,9 +10,11 @@ module JSI
|
|
13
10
|
# are dynamically created for schemas using {JSI.class_for_schema}, and these
|
14
11
|
# are what are used to instantiate and represent JSON schema instances.
|
15
12
|
class Base
|
16
|
-
include Memoize
|
13
|
+
include Util::Memoize
|
17
14
|
include Enumerable
|
18
15
|
include PathedNode
|
16
|
+
class CannotSubscriptError < StandardError
|
17
|
+
end
|
19
18
|
|
20
19
|
class << self
|
21
20
|
# JSI::Base.new_jsi behaves the same as .new, and is defined for compatibility so you may call #new_jsi
|
@@ -33,43 +32,71 @@ module JSI
|
|
33
32
|
@in_schema_classes
|
34
33
|
end
|
35
34
|
|
36
|
-
# @return [String]
|
37
|
-
#
|
38
|
-
def schema_id
|
39
|
-
schema.schema_id
|
40
|
-
end
|
41
|
-
|
42
|
-
# @return [String] a string representing the class, with schema_id or schema ptr fragment
|
35
|
+
# @return [String] a string representing the class, indicating the schemas represented by their module
|
36
|
+
# name or a URI
|
43
37
|
def inspect
|
44
|
-
if !respond_to?(:
|
38
|
+
if !respond_to?(:jsi_class_schemas)
|
45
39
|
super
|
46
40
|
else
|
47
|
-
|
41
|
+
schema_names = jsi_class_schemas.map do |schema|
|
42
|
+
mod = schema.jsi_schema_module
|
43
|
+
if mod.name && schema.schema_id
|
44
|
+
"#{mod.name} (#{schema.schema_id})"
|
45
|
+
elsif mod.name
|
46
|
+
mod.name
|
47
|
+
elsif schema.schema_id
|
48
|
+
schema.schema_id
|
49
|
+
else
|
50
|
+
schema.node_ptr.uri
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
48
54
|
if name && !in_schema_classes
|
49
|
-
|
55
|
+
if jsi_class_schemas.empty?
|
56
|
+
"#{name} (0 schemas)"
|
57
|
+
else
|
58
|
+
"#{name} (#{schema_names.join(', ')})"
|
59
|
+
end
|
50
60
|
else
|
51
|
-
|
61
|
+
if schema_names.empty?
|
62
|
+
"(JSI Schema Class for 0 schemas)"
|
63
|
+
else
|
64
|
+
"(JSI Schema Class: #{schema_names.join(', ')})"
|
65
|
+
end
|
52
66
|
end
|
53
67
|
end
|
54
68
|
end
|
55
69
|
|
56
70
|
alias_method :to_s, :inspect
|
57
71
|
|
58
|
-
# @return [String] a name for a constant for this class, generated from the
|
59
|
-
#
|
72
|
+
# @return [String, nil] a name for a constant for this class, generated from the constant name
|
73
|
+
# or schema id of each schema this class represents. nil if any represented schema has no constant
|
74
|
+
# name or schema id.
|
60
75
|
def schema_classes_const_name
|
61
|
-
if
|
62
|
-
|
76
|
+
if respond_to?(:jsi_class_schemas)
|
77
|
+
schema_names = jsi_class_schemas.map do |schema|
|
78
|
+
if schema.jsi_schema_module.name
|
79
|
+
schema.jsi_schema_module.name
|
80
|
+
elsif schema.schema_id
|
81
|
+
schema.schema_id
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if !schema_names.any?(&:nil?) && !schema_names.empty?
|
87
|
+
schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('')
|
88
|
+
end
|
63
89
|
end
|
64
90
|
end
|
65
91
|
|
66
92
|
# @return [String] a constant name of this class
|
67
93
|
def name
|
68
94
|
unless instance_variable_defined?(:@in_schema_classes)
|
69
|
-
|
95
|
+
const_name = schema_classes_const_name
|
96
|
+
if super || !const_name || SchemaClasses.const_defined?(const_name)
|
70
97
|
@in_schema_classes = false
|
71
98
|
else
|
72
|
-
SchemaClasses.const_set(
|
99
|
+
SchemaClasses.const_set(const_name, self)
|
73
100
|
@in_schema_classes = true
|
74
101
|
end
|
75
102
|
end
|
@@ -95,8 +122,8 @@ module JSI
|
|
95
122
|
# iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
|
96
123
|
# @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
|
97
124
|
def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil)
|
98
|
-
unless respond_to?(:
|
99
|
-
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #
|
125
|
+
unless respond_to?(:jsi_schemas)
|
126
|
+
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
|
100
127
|
end
|
101
128
|
|
102
129
|
if instance.is_a?(JSI::Schema)
|
@@ -126,17 +153,20 @@ module JSI
|
|
126
153
|
else
|
127
154
|
raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
|
128
155
|
@jsi_document = instance
|
129
|
-
@jsi_ptr = JSI::JSON::Pointer
|
156
|
+
@jsi_ptr = JSI::JSON::Pointer[]
|
130
157
|
@jsi_root_node = self
|
131
158
|
end
|
132
159
|
|
133
160
|
if self.jsi_instance.respond_to?(:to_hash)
|
134
|
-
extend
|
161
|
+
extend PathedHashNode
|
135
162
|
elsif self.jsi_instance.respond_to?(:to_ary)
|
136
|
-
extend
|
163
|
+
extend PathedArrayNode
|
137
164
|
end
|
138
|
-
|
139
|
-
|
165
|
+
|
166
|
+
jsi_schemas.each do |schema|
|
167
|
+
if schema.describes_schema?
|
168
|
+
extend JSI::Schema
|
169
|
+
end
|
140
170
|
end
|
141
171
|
end
|
142
172
|
|
@@ -153,11 +183,11 @@ module JSI
|
|
153
183
|
alias_method :node_ptr, :jsi_ptr
|
154
184
|
alias_method :document_root_node, :jsi_root_node
|
155
185
|
|
156
|
-
# the instance of the json-schema
|
186
|
+
# the instance of the json-schema - the underlying JSON data used to instantiate this JSI
|
157
187
|
alias_method :jsi_instance, :node_content
|
158
188
|
alias_method :instance, :node_content
|
159
189
|
|
160
|
-
# each is overridden by
|
190
|
+
# each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base
|
161
191
|
# #each is not actually implemented, along with all the methods of Enumerable.
|
162
192
|
def each
|
163
193
|
raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
|
@@ -192,8 +222,9 @@ module JSI
|
|
192
222
|
|
193
223
|
# @param token [String, Integer, Object] the token to subscript
|
194
224
|
# @return [JSI::Base, Object] the instance's subscript value at the given token.
|
195
|
-
# if
|
196
|
-
# returns
|
225
|
+
# if this JSI's schemas define subschemas which apply for the given token, and the value is complex,
|
226
|
+
# returns the subscript value as a JSI instantiation of those subschemas. otherwise, the plain instance
|
227
|
+
# value is returned.
|
197
228
|
def [](token)
|
198
229
|
if respond_to?(:to_hash)
|
199
230
|
token_in_range = node_content_hash_pubsend(:key?, token)
|
@@ -202,29 +233,29 @@ module JSI
|
|
202
233
|
token_in_range = node_content_ary_pubsend(:each_index).include?(token)
|
203
234
|
value = node_content_ary_pubsend(:[], token)
|
204
235
|
else
|
205
|
-
raise(
|
236
|
+
raise(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
|
206
237
|
end
|
207
238
|
|
208
|
-
jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
|
239
|
+
result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
|
209
240
|
if respond_to?(:to_ary)
|
210
|
-
|
241
|
+
token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_index(token) }.inject(Set.new, &:|)
|
211
242
|
else
|
212
|
-
|
243
|
+
token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_property_name(token) }.inject(Set.new, &:|)
|
213
244
|
end
|
214
|
-
|
245
|
+
token_schemas = token_schemas.map { |schema| schema.match_to_instance(value) }.inject(Set.new, &:|)
|
215
246
|
|
216
247
|
if token_in_range
|
217
|
-
complex_value =
|
218
|
-
schema_value =
|
248
|
+
complex_value = token_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
|
249
|
+
schema_value = token_schemas.any? { |token_schema| token_schema.describes_schema? }
|
219
250
|
|
220
251
|
if complex_value || schema_value
|
221
|
-
|
252
|
+
JSI::SchemaClasses.class_for_schemas(token_schemas).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
|
222
253
|
else
|
223
254
|
value
|
224
255
|
end
|
225
256
|
else
|
226
257
|
defaults = Set.new
|
227
|
-
|
258
|
+
token_schemas.each do |token_schema|
|
228
259
|
if token_schema.respond_to?(:to_hash) && token_schema.key?('default')
|
229
260
|
defaults << token_schema['default']
|
230
261
|
end
|
@@ -243,6 +274,7 @@ module JSI
|
|
243
274
|
end
|
244
275
|
end
|
245
276
|
end
|
277
|
+
result
|
246
278
|
end
|
247
279
|
|
248
280
|
# assigns the subscript of the instance identified by the given token to the given value.
|
@@ -297,12 +329,12 @@ module JSI
|
|
297
329
|
|
298
330
|
# @return [Array] array of schema validation errors for this instance
|
299
331
|
def fully_validate(errors_as_objects: false)
|
300
|
-
schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects)
|
332
|
+
jsi_schemas.map { |schema| schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects) }.inject([], &:+)
|
301
333
|
end
|
302
334
|
|
303
335
|
# @return [true, false] whether the instance validates against its schema
|
304
336
|
def validate
|
305
|
-
schema.validate_instance(jsi_instance)
|
337
|
+
jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) }
|
306
338
|
end
|
307
339
|
|
308
340
|
# @return [true] if this method does not raise, it returns true to
|
@@ -310,7 +342,8 @@ module JSI
|
|
310
342
|
# @raise [::JSON::Schema::ValidationError] raises if the instance has
|
311
343
|
# validation errors
|
312
344
|
def validate!
|
313
|
-
schema.validate_instance!(jsi_instance)
|
345
|
+
jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) }
|
346
|
+
true
|
314
347
|
end
|
315
348
|
|
316
349
|
def dup
|
@@ -344,18 +377,18 @@ module JSI
|
|
344
377
|
class_txt = begin
|
345
378
|
if class_name
|
346
379
|
# ignore ID
|
347
|
-
|
348
|
-
if
|
380
|
+
schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
|
381
|
+
if schema_module_names.empty?
|
349
382
|
class_name
|
350
383
|
else
|
351
|
-
"#{class_name} (#{
|
384
|
+
"#{class_name} (#{schema_module_names.join(', ')})"
|
352
385
|
end
|
353
386
|
else
|
354
|
-
|
355
|
-
if
|
387
|
+
schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact
|
388
|
+
if schema_names.empty?
|
356
389
|
"JSI"
|
357
390
|
else
|
358
|
-
"JSI (#{
|
391
|
+
"JSI (#{schema_names.join(', ')})"
|
359
392
|
end
|
360
393
|
end
|
361
394
|
end
|
@@ -387,24 +420,6 @@ module JSI
|
|
387
420
|
def jsi_fingerprint
|
388
421
|
{class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
|
389
422
|
end
|
390
|
-
include FingerprintHash
|
391
|
-
|
392
|
-
private
|
393
|
-
|
394
|
-
# this is an instance method in order to allow subclasses of JSI classes to
|
395
|
-
# override it to point to other subclasses corresponding to other schemas.
|
396
|
-
def class_for_schema(schema)
|
397
|
-
JSI.class_for_schema(schema)
|
398
|
-
end
|
399
|
-
end
|
400
|
-
|
401
|
-
# module extending a {JSI::Base} object when its instance is Hash-like (responds to #to_hash)
|
402
|
-
module BaseHash
|
403
|
-
include PathedHashNode
|
404
|
-
end
|
405
|
-
|
406
|
-
# module extending a {JSI::Base} object when its instance is Array-like (responds to #to_ary)
|
407
|
-
module BaseArray
|
408
|
-
include PathedArrayNode
|
423
|
+
include Util::FingerprintHash
|
409
424
|
end
|
410
425
|
end
|
data/lib/jsi/jsi_coder.rb
CHANGED
@@ -14,11 +14,11 @@ module JSI
|
|
14
14
|
# Preferences = JSI.class_for_schema(preferences_json_schema)
|
15
15
|
# class Foo < ActiveRecord::Base
|
16
16
|
# # as a single serializer, loads a Preferences instance from a json column
|
17
|
-
# serialize '
|
17
|
+
# serialize 'preferences_json', JSI::JSICoder.new(Preferences)
|
18
18
|
#
|
19
19
|
# # for a text column, arms_serialize will go from JSI to JSON-compatible
|
20
20
|
# # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
|
21
|
-
# arms_serialize '
|
21
|
+
# arms_serialize 'preferences_txt', [:jsi, Preferences], :json
|
22
22
|
# end
|
23
23
|
#
|
24
24
|
# the column data may be either a single instance of the schema class
|
data/lib/jsi/json/node.rb
CHANGED
@@ -155,7 +155,7 @@ module JSI
|
|
155
155
|
def object_group_text
|
156
156
|
[
|
157
157
|
self.class.inspect,
|
158
|
-
|
158
|
+
node_ptr.uri.to_s,
|
159
159
|
] + (node_content.respond_to?(:object_group_text) ? node_content.object_group_text : [])
|
160
160
|
end
|
161
161
|
|
@@ -185,7 +185,7 @@ module JSI
|
|
185
185
|
def jsi_fingerprint
|
186
186
|
{class: JSI::JSON::Node, node_document: node_document, node_ptr: node_ptr}
|
187
187
|
end
|
188
|
-
include FingerprintHash
|
188
|
+
include Util::FingerprintHash
|
189
189
|
end
|
190
190
|
|
191
191
|
# a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
|
data/lib/jsi/json/pointer.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'addressable/uri'
|
4
|
-
|
5
3
|
module JSI
|
6
4
|
module JSON
|
7
5
|
# a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
|
@@ -33,28 +31,23 @@ module JSI
|
|
33
31
|
|
34
32
|
# parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer
|
35
33
|
#
|
36
|
-
# ptr = JSI::JSON::Pointer.from_fragment('
|
37
|
-
# => #<JSI::JSON::Pointer fragment:
|
34
|
+
# ptr = JSI::JSON::Pointer.from_fragment('/foo/bar')
|
35
|
+
# => #<JSI::JSON::Pointer fragment: /foo/bar>
|
38
36
|
# ptr.reference_tokens
|
39
37
|
# => ["foo", "bar"]
|
40
38
|
#
|
41
39
|
# with URI escaping:
|
42
40
|
#
|
43
|
-
# ptr = JSI::JSON::Pointer.from_fragment('
|
44
|
-
# => #<JSI::JSON::Pointer fragment:
|
41
|
+
# ptr = JSI::JSON::Pointer.from_fragment('/foo%20bar')
|
42
|
+
# => #<JSI::JSON::Pointer fragment: /foo%20bar>
|
45
43
|
# ptr.reference_tokens
|
46
44
|
# => ["foo bar"]
|
47
45
|
#
|
48
46
|
# @param fragment [String] a fragment containing a pointer (starting with #)
|
49
47
|
# @return [JSI::JSON::Pointer]
|
48
|
+
# @raise [JSI::JSON::Pointer::PointerSyntaxError] when the fragment does not contain a pointer with valid pointer syntax
|
50
49
|
def self.from_fragment(fragment)
|
51
|
-
|
52
|
-
match = fragment.match(/\A#/)
|
53
|
-
if match
|
54
|
-
from_pointer(match.post_match, type: 'fragment')
|
55
|
-
else
|
56
|
-
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
|
57
|
-
end
|
50
|
+
from_pointer(Addressable::URI.unescape(fragment), type: 'fragment')
|
58
51
|
end
|
59
52
|
|
60
53
|
# parse a pointer string and instantiate as a JSI::JSON::Pointer
|
@@ -72,6 +65,7 @@ module JSI
|
|
72
65
|
# @param pointer_string [String] a pointer string
|
73
66
|
# @param type (for internal use) indicates the original representation of the pointer
|
74
67
|
# @return [JSI::JSON::Pointer]
|
68
|
+
# @raise [JSI::JSON::Pointer::PointerSyntaxError] when the pointer_string does not have valid pointer syntax
|
75
69
|
def self.from_pointer(pointer_string, type: 'pointer')
|
76
70
|
tokens = pointer_string.split('/', -1).map! do |piece|
|
77
71
|
piece.gsub('~1', '/').gsub('~0', '~')
|
@@ -137,7 +131,12 @@ module JSI
|
|
137
131
|
|
138
132
|
# @return [String] the fragment string representation of this Pointer
|
139
133
|
def fragment
|
140
|
-
|
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)
|
141
140
|
end
|
142
141
|
|
143
142
|
# @return [Boolean] whether this pointer points to the root (has an empty array of reference_tokens)
|
@@ -197,102 +196,108 @@ module JSI
|
|
197
196
|
Pointer.new(reference_tokens + [token], type: @type)
|
198
197
|
end
|
199
198
|
|
200
|
-
# given this Pointer points to a schema in the given document, returns a
|
201
|
-
# to
|
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.
|
202
201
|
#
|
203
202
|
# @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
|
204
203
|
# @param property_name [Object] the property name for which to find a subschema
|
205
|
-
# @return [JSI::JSON::Pointer
|
206
|
-
def
|
204
|
+
# @return [Set<JSI::JSON::Pointer>] pointers to subschemas
|
205
|
+
def schema_subschema_ptrs_for_property_name(document, property_name)
|
207
206
|
ptr = self
|
208
207
|
schema = ptr.evaluate(document)
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
216
215
|
if schema['patternProperties'].respond_to?(:to_hash)
|
217
|
-
|
218
|
-
property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
|
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
|
219
221
|
end
|
220
222
|
end
|
221
|
-
if
|
222
|
-
ptr['
|
223
|
-
else
|
224
|
-
if schema.key?('additionalProperties')
|
225
|
-
ptr['additionalProperties']
|
226
|
-
else
|
227
|
-
nil
|
228
|
-
end
|
223
|
+
if apply_additional && schema.key?('additionalProperties')
|
224
|
+
ptrs << ptr['additionalProperties']
|
229
225
|
end
|
230
226
|
end
|
231
227
|
end
|
232
228
|
end
|
233
229
|
|
234
|
-
# given this Pointer points to a schema in the given document, returns a
|
235
|
-
# to
|
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.
|
236
232
|
#
|
237
233
|
# @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
|
238
|
-
# @param idx [Object] the array index for which to find
|
239
|
-
# @return [JSI::JSON::Pointer
|
240
|
-
def
|
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)
|
241
237
|
ptr = self
|
242
238
|
schema = ptr.evaluate(document)
|
243
|
-
|
244
|
-
|
245
|
-
else
|
246
|
-
if schema.key?('items') || schema.key?('additionalItems')
|
239
|
+
Set.new.tap do |ptrs|
|
240
|
+
if schema.respond_to?(:to_hash)
|
247
241
|
if schema['items'].respond_to?(:to_ary)
|
248
242
|
if schema['items'].each_index.to_a.include?(idx)
|
249
|
-
ptr['items'][idx]
|
243
|
+
ptrs << ptr['items'][idx]
|
250
244
|
elsif schema.key?('additionalItems')
|
251
|
-
ptr['additionalItems']
|
252
|
-
else
|
253
|
-
nil
|
245
|
+
ptrs << ptr['additionalItems']
|
254
246
|
end
|
255
247
|
elsif schema.key?('items')
|
256
|
-
ptr['items']
|
257
|
-
else
|
258
|
-
nil
|
248
|
+
ptrs << ptr['items']
|
259
249
|
end
|
260
|
-
else
|
261
|
-
nil
|
262
250
|
end
|
263
251
|
end
|
264
252
|
end
|
265
253
|
|
266
|
-
# given this Pointer points to a schema in the given document, this matches
|
267
|
-
#
|
268
|
-
#
|
269
|
-
# self is returned.
|
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.
|
270
257
|
#
|
271
|
-
# @param document [#to_hash, #to_ary, Object] document containing the schema
|
272
|
-
#
|
273
|
-
# @param instance [Object] the instance to which to attempt to match *Of subschemas
|
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
|
274
260
|
# @return [JSI::JSON::Pointer] either a pointer to a *Of subschema in the document,
|
275
261
|
# or self if no other subschema was matched
|
276
|
-
def
|
262
|
+
def schema_match_ptrs_to_instance(document, instance)
|
277
263
|
ptr = self
|
278
264
|
schema = ptr.evaluate(document)
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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))
|
291
294
|
end
|
292
295
|
end
|
296
|
+
# TODO dependencies
|
297
|
+
else
|
298
|
+
ptrs << ptr
|
293
299
|
end
|
294
300
|
end
|
295
|
-
return ptr
|
296
301
|
end
|
297
302
|
|
298
303
|
# takes a document and a block. the block is yielded the content of the given document at this
|
@@ -378,7 +383,7 @@ module JSI
|
|
378
383
|
return self unless ref.is_a?(String)
|
379
384
|
|
380
385
|
if ref[/\A#/]
|
381
|
-
return Pointer.from_fragment(ref).tap(&block)
|
386
|
+
return Pointer.from_fragment(Addressable::URI.parse(ref).fragment).tap(&block)
|
382
387
|
end
|
383
388
|
|
384
389
|
# HAX for how google does refs and ids
|
@@ -399,7 +404,7 @@ module JSI
|
|
399
404
|
|
400
405
|
# @return [String] string representation of this Pointer
|
401
406
|
def inspect
|
402
|
-
"
|
407
|
+
"#{self.class.name}[#{reference_tokens.map(&:inspect).join(", ")}]"
|
403
408
|
end
|
404
409
|
|
405
410
|
alias_method :to_s, :inspect
|
@@ -408,20 +413,7 @@ module JSI
|
|
408
413
|
def jsi_fingerprint
|
409
414
|
{class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
|
410
415
|
end
|
411
|
-
include FingerprintHash
|
412
|
-
|
413
|
-
private
|
414
|
-
|
415
|
-
# @return [String] a representation of this pointer based on @type
|
416
|
-
def representation_s
|
417
|
-
if @type == 'fragment'
|
418
|
-
"fragment: #{fragment}"
|
419
|
-
elsif @type == 'pointer'
|
420
|
-
"pointer: #{pointer}"
|
421
|
-
else
|
422
|
-
"reference_tokens: #{reference_tokens.inspect}"
|
423
|
-
end
|
424
|
-
end
|
416
|
+
include Util::FingerprintHash
|
425
417
|
end
|
426
418
|
end
|
427
419
|
end
|