jsi 0.2.1 → 0.3.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/CHANGELOG.md +8 -0
- data/LICENSE.md +613 -0
- data/README.md +62 -37
- data/jsi.gemspec +8 -12
- data/lib/jsi.rb +11 -0
- data/lib/jsi/base.rb +196 -258
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +20 -15
- data/lib/jsi/json-schema-fragments.rb +2 -0
- data/lib/jsi/json.rb +2 -0
- data/lib/jsi/json/node.rb +45 -88
- data/lib/jsi/json/pointer.rb +102 -5
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +217 -0
- data/lib/jsi/pathed_node.rb +5 -0
- data/lib/jsi/schema.rb +146 -169
- data/lib/jsi/schema_classes.rb +112 -47
- data/lib/jsi/simple_wrap.rb +8 -3
- data/lib/jsi/typelike_modules.rb +31 -39
- data/lib/jsi/util.rb +27 -47
- 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 +174 -60
- data/test/base_hash_test.rb +179 -46
- data/test/base_test.rb +163 -94
- data/test/jsi_coder_test.rb +14 -14
- data/test/jsi_json_arraynode_test.rb +10 -10
- data/test/jsi_json_hashnode_test.rb +14 -14
- data/test/jsi_json_node_test.rb +83 -136
- data/test/jsi_typelike_as_json_test.rb +1 -1
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +40 -50
- data/test/test_helper.rb +35 -3
- data/test/util_test.rb +8 -8
- metadata +24 -16
- data/LICENSE.txt +0 -21
data/lib/jsi/base/to_rb.rb
CHANGED
data/lib/jsi/jsi_coder.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
# this is an ActiveRecord serialization coder intended to serialize between
|
3
5
|
# JSON-compatible objects on the database side, and a JSI instance loaded on
|
@@ -19,24 +21,27 @@ module JSI
|
|
19
21
|
# arms_serialize 'preferences', [:jsi, Preferences], :json
|
20
22
|
# end
|
21
23
|
#
|
22
|
-
# the column data may be either a single instance of the
|
24
|
+
# the column data may be either a single instance of the schema class
|
23
25
|
# (represented as one json object) or an array of them (represented as a json
|
24
26
|
# array of json objects), indicated by the keyword argument `array`.
|
25
27
|
class JSICoder
|
26
|
-
# @param
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# an array
|
30
|
-
def initialize(
|
31
|
-
|
28
|
+
# @param schema [JSI::Schema, JSI::SchemaModule, Class < JSI::Base] a schema, a JSI schema class, or
|
29
|
+
# a JSI schema module. #load will instantiate column data using the JSI schema represented.
|
30
|
+
# @param array [Boolean] whether the dumped data represent one instance of the schema,
|
31
|
+
# or an array of them. note that it may be preferable to simply use an array schema.
|
32
|
+
def initialize(schema, array: false)
|
33
|
+
unless schema.respond_to?(:new_jsi)
|
34
|
+
raise(ArgumentError, "not a JSI schema, class, or module: #{schema.inspect}")
|
35
|
+
end
|
36
|
+
@schema = schema
|
32
37
|
@array = array
|
33
38
|
end
|
34
39
|
|
35
|
-
# loads the database column to instances of
|
40
|
+
# loads the database column to JSI instances of our schema
|
36
41
|
#
|
37
42
|
# @param data [Object, Array, nil] the dumped schema instance(s) of the JSI(s)
|
38
|
-
# @return [
|
39
|
-
#
|
43
|
+
# @return [JSI::Base, Array<JSI::Base>, nil] the JSI or JSIs containing the schema
|
44
|
+
# instance(s), or nil if data is nil
|
40
45
|
def load(data)
|
41
46
|
return nil if data.nil?
|
42
47
|
object = if @array
|
@@ -50,8 +55,8 @@ module JSI
|
|
50
55
|
object
|
51
56
|
end
|
52
57
|
|
53
|
-
# @param object [
|
54
|
-
#
|
58
|
+
# @param object [JSI::Base, Array<JSI::Base>, nil] the JSI or array of JSIs containing
|
59
|
+
# the schema instance(s)
|
55
60
|
# @return [Object, Array, nil] the schema instance(s) of the JSI(s), or nil if object is nil
|
56
61
|
def dump(object)
|
57
62
|
return nil if object.nil?
|
@@ -72,12 +77,12 @@ module JSI
|
|
72
77
|
|
73
78
|
private
|
74
79
|
# @param data [Object]
|
75
|
-
# @return [
|
80
|
+
# @return [JSI::Base]
|
76
81
|
def load_object(data)
|
77
|
-
@
|
82
|
+
@schema.new_jsi(data)
|
78
83
|
end
|
79
84
|
|
80
|
-
# @param object [
|
85
|
+
# @param object [JSI::Base, Object]
|
81
86
|
# @return [Object]
|
82
87
|
def dump_object(object)
|
83
88
|
JSI::Typelike.as_json(object)
|
data/lib/jsi/json.rb
CHANGED
data/lib/jsi/json/node.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
module JSON
|
3
5
|
# JSI::JSON::Node is an abstraction of a node within a JSON document.
|
@@ -22,55 +24,45 @@ module JSI
|
|
22
24
|
# return a copy of the document with the content of the node modified.
|
23
25
|
# the original node's document and content are untouched.
|
24
26
|
class Node
|
27
|
+
include Enumerable
|
25
28
|
include PathedNode
|
26
29
|
|
27
|
-
def self.new_doc(
|
28
|
-
new_by_type(
|
30
|
+
def self.new_doc(node_document)
|
31
|
+
new_by_type(node_document, JSI::JSON::Pointer.new([]))
|
29
32
|
end
|
30
33
|
|
31
34
|
# if the content of the document at the given pointer is Hash-like, returns
|
32
35
|
# a HashNode; if Array-like, returns ArrayNode. otherwise returns a
|
33
36
|
# regular Node, although Nodes are for the most part instantiated from
|
34
37
|
# Hash or Array-like content.
|
35
|
-
def self.new_by_type(
|
36
|
-
content =
|
38
|
+
def self.new_by_type(node_document, node_ptr)
|
39
|
+
content = node_ptr.evaluate(node_document)
|
37
40
|
if content.respond_to?(:to_hash)
|
38
|
-
HashNode.new(
|
41
|
+
HashNode.new(node_document, node_ptr)
|
39
42
|
elsif content.respond_to?(:to_ary)
|
40
|
-
ArrayNode.new(
|
43
|
+
ArrayNode.new(node_document, node_ptr)
|
41
44
|
else
|
42
|
-
Node.new(
|
45
|
+
Node.new(node_document, node_ptr)
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
46
49
|
# a Node represents the content of a document at a given pointer.
|
47
|
-
def initialize(
|
48
|
-
unless
|
49
|
-
raise(TypeError, "
|
50
|
+
def initialize(node_document, node_ptr)
|
51
|
+
unless node_ptr.is_a?(JSI::JSON::Pointer)
|
52
|
+
raise(TypeError, "node_ptr must be a JSI::JSON::Pointer. got: #{node_ptr.pretty_inspect.chomp} (#{node_ptr.class})")
|
50
53
|
end
|
51
|
-
if
|
52
|
-
raise(TypeError, "
|
54
|
+
if node_document.is_a?(JSI::JSON::Node)
|
55
|
+
raise(TypeError, "node_document of a Node should not be another JSI::JSON::Node: #{node_document.inspect}")
|
53
56
|
end
|
54
|
-
@
|
55
|
-
@
|
57
|
+
@node_document = node_document
|
58
|
+
@node_ptr = node_ptr
|
56
59
|
end
|
57
60
|
|
58
61
|
# the document containing this Node at our pointer
|
59
|
-
attr_reader :
|
62
|
+
attr_reader :node_document
|
60
63
|
|
61
64
|
# JSI::JSON::Pointer pointing to this node within its document
|
62
|
-
attr_reader :
|
63
|
-
|
64
|
-
# @return [Array<Object>] the path of this node; an array of reference_tokens of the pointer
|
65
|
-
def path
|
66
|
-
pointer.reference_tokens
|
67
|
-
end
|
68
|
-
|
69
|
-
alias_method :node_document, :document
|
70
|
-
alias_method :node_ptr, :pointer
|
71
|
-
|
72
|
-
# the raw content of this Node from the underlying document at this Node's pointer.
|
73
|
-
alias_method :content, :node_content
|
65
|
+
attr_reader :node_ptr
|
74
66
|
|
75
67
|
# returns content at the given subscript - call this the subcontent.
|
76
68
|
#
|
@@ -82,14 +74,8 @@ module JSI
|
|
82
74
|
# if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
|
83
75
|
# not a key of the hash, then the $ref is followed before returning the subcontent.
|
84
76
|
def [](subscript)
|
85
|
-
ptr = self.
|
86
|
-
content = self.
|
87
|
-
if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
|
88
|
-
pointer.deref(document) do |deref_ptr|
|
89
|
-
ptr = deref_ptr
|
90
|
-
content = ptr.evaluate(document)
|
91
|
-
end
|
92
|
-
end
|
77
|
+
ptr = self.node_ptr
|
78
|
+
content = self.node_content
|
93
79
|
unless content.respond_to?(:[])
|
94
80
|
if content.respond_to?(:to_hash)
|
95
81
|
content = content.to_hash
|
@@ -105,9 +91,9 @@ module JSI
|
|
105
91
|
raise(e.class, e.message + "\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}", e.backtrace)
|
106
92
|
end
|
107
93
|
if subcontent.respond_to?(:to_hash)
|
108
|
-
HashNode.new(
|
94
|
+
HashNode.new(node_document, ptr[subscript])
|
109
95
|
elsif subcontent.respond_to?(:to_ary)
|
110
|
-
ArrayNode.new(
|
96
|
+
ArrayNode.new(node_document, ptr[subscript])
|
111
97
|
else
|
112
98
|
subcontent
|
113
99
|
end
|
@@ -116,9 +102,9 @@ module JSI
|
|
116
102
|
# assigns the given subscript of the content to the given value. the document is modified in place.
|
117
103
|
def []=(subscript, value)
|
118
104
|
if value.is_a?(Node)
|
119
|
-
|
105
|
+
node_content[subscript] = value.node_content
|
120
106
|
else
|
121
|
-
|
107
|
+
node_content[subscript] = value
|
122
108
|
end
|
123
109
|
end
|
124
110
|
|
@@ -132,49 +118,32 @@ module JSI
|
|
132
118
|
# (e.g. a $ref to an external document, which is not yet supported), the block is not called.
|
133
119
|
# @return [JSI::JSON::Node] dereferenced node, or this node
|
134
120
|
def deref(&block)
|
135
|
-
|
136
|
-
return Node.new_by_type(
|
121
|
+
node_ptr_deref do |deref_ptr|
|
122
|
+
return Node.new_by_type(node_document, deref_ptr).tap(&(block || Util::NOOP))
|
137
123
|
end
|
138
124
|
return self
|
139
125
|
end
|
140
126
|
|
141
127
|
# a Node at the root of the document
|
142
|
-
def
|
143
|
-
Node.new_doc(
|
144
|
-
end
|
145
|
-
|
146
|
-
alias_method :document_root_node, :document_node
|
147
|
-
|
148
|
-
# @return [Boolean] whether this node is the root of its document
|
149
|
-
def root_node?
|
150
|
-
pointer.root?
|
128
|
+
def document_root_node
|
129
|
+
Node.new_doc(node_document)
|
151
130
|
end
|
152
131
|
|
153
132
|
# the parent of this node. if this node is the document root, raises
|
154
133
|
# JSI::JSON::Pointer::ReferenceError.
|
155
134
|
def parent_node
|
156
|
-
Node.new_by_type(
|
157
|
-
end
|
158
|
-
|
159
|
-
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
160
|
-
def pointer_path
|
161
|
-
pointer.pointer
|
162
|
-
end
|
163
|
-
|
164
|
-
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
165
|
-
def fragment
|
166
|
-
pointer.fragment
|
135
|
+
Node.new_by_type(node_document, node_ptr.parent)
|
167
136
|
end
|
168
137
|
|
169
138
|
# returns a jsonifiable representation of this node's content
|
170
139
|
def as_json(*opt)
|
171
|
-
Typelike.as_json(
|
140
|
+
Typelike.as_json(node_content, *opt)
|
172
141
|
end
|
173
142
|
|
174
143
|
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
175
144
|
# copy of that content (and NOT modify the object it is given).
|
176
145
|
def modified_copy(&block)
|
177
|
-
Node.new_by_type(
|
146
|
+
Node.new_by_type(node_ptr.modified_document_copy(node_document, &block), node_ptr)
|
178
147
|
end
|
179
148
|
|
180
149
|
def dup
|
@@ -185,36 +154,36 @@ module JSI
|
|
185
154
|
# @return [Array<String>]
|
186
155
|
def object_group_text
|
187
156
|
[
|
157
|
+
self.class.inspect,
|
188
158
|
"fragment=#{node_ptr.fragment.inspect}",
|
189
159
|
] + (node_content.respond_to?(:object_group_text) ? node_content.object_group_text : [])
|
190
160
|
end
|
191
161
|
|
192
162
|
# a string representing this node
|
193
163
|
def inspect
|
194
|
-
"\#<#{
|
164
|
+
"\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
|
195
165
|
end
|
196
166
|
|
197
167
|
# pretty-prints a representation this node to the given printer
|
198
168
|
def pretty_print(q)
|
199
|
-
q.
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
}
|
169
|
+
q.text '#<'
|
170
|
+
q.text object_group_text.join(' ')
|
171
|
+
q.group_sub {
|
172
|
+
q.nest(2) {
|
173
|
+
q.breakable ' '
|
174
|
+
q.pp node_content
|
206
175
|
}
|
207
|
-
|
208
|
-
|
209
|
-
|
176
|
+
}
|
177
|
+
q.breakable ''
|
178
|
+
q.text '>'
|
210
179
|
end
|
211
180
|
|
212
181
|
# fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
|
213
182
|
# (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
|
214
183
|
# documents at equal pointers. note that this means two nodes with the same content may not be
|
215
184
|
# considered equal.
|
216
|
-
def
|
217
|
-
{class: JSI::JSON::Node,
|
185
|
+
def jsi_fingerprint
|
186
|
+
{class: JSI::JSON::Node, node_document: node_document, node_ptr: node_ptr}
|
218
187
|
end
|
219
188
|
include FingerprintHash
|
220
189
|
end
|
@@ -222,25 +191,13 @@ module JSI
|
|
222
191
|
# a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
|
223
192
|
# and includes Array methods from Arraylike
|
224
193
|
class ArrayNode < Node
|
225
|
-
include Enumerable
|
226
194
|
include PathedArrayNode
|
227
|
-
|
228
|
-
# returns a jsonifiable representation of this node's content
|
229
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
230
|
-
Typelike.as_json(content, *opt)
|
231
|
-
end
|
232
195
|
end
|
233
196
|
|
234
197
|
# a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
|
235
198
|
# and includes Hash methods from Hashlike
|
236
199
|
class HashNode < Node
|
237
|
-
include Enumerable
|
238
200
|
include PathedHashNode
|
239
|
-
|
240
|
-
# returns a jsonifiable representation of this node's content
|
241
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
242
|
-
Typelike.as_json(content, *opt)
|
243
|
-
end
|
244
201
|
end
|
245
202
|
end
|
246
203
|
end
|
data/lib/jsi/json/pointer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'addressable/uri'
|
2
4
|
|
3
5
|
module JSI
|
@@ -195,6 +197,104 @@ module JSI
|
|
195
197
|
Pointer.new(reference_tokens + [token], type: @type)
|
196
198
|
end
|
197
199
|
|
200
|
+
# given this Pointer points to a schema in the given document, returns a pointer
|
201
|
+
# to a subschema of that schema for the given property name.
|
202
|
+
#
|
203
|
+
# @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
|
204
|
+
# @param property_name [Object] the property name for which to find a subschema
|
205
|
+
# @return [JSI::JSON::Pointer, nil] a pointer to a subschema in the document for the property_name, or nil
|
206
|
+
def schema_subschema_ptr_for_property_name(document, property_name)
|
207
|
+
ptr = self
|
208
|
+
schema = ptr.evaluate(document)
|
209
|
+
if !schema.respond_to?(:to_hash)
|
210
|
+
nil
|
211
|
+
else
|
212
|
+
if schema.key?('properties') && schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(property_name)
|
213
|
+
ptr['properties'][property_name]
|
214
|
+
else
|
215
|
+
# TODO this is rather incorrect handling of patternProperties and additionalProperties
|
216
|
+
if schema['patternProperties'].respond_to?(:to_hash)
|
217
|
+
pattern_schema_name = schema['patternProperties'].keys.detect do |pattern|
|
218
|
+
property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
|
219
|
+
end
|
220
|
+
end
|
221
|
+
if pattern_schema_name
|
222
|
+
ptr['patternProperties'][pattern_schema_name]
|
223
|
+
else
|
224
|
+
if schema.key?('additionalProperties')
|
225
|
+
ptr['additionalProperties']
|
226
|
+
else
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# given this Pointer points to a schema in the given document, returns a pointer
|
235
|
+
# to a subschema of that schema for the given array index.
|
236
|
+
#
|
237
|
+
# @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 a subschema
|
239
|
+
# @return [JSI::JSON::Pointer, nil] a pointer to a subschema in the document for array index idx, or nil
|
240
|
+
def schema_subschema_ptr_for_index(document, idx)
|
241
|
+
ptr = self
|
242
|
+
schema = ptr.evaluate(document)
|
243
|
+
if !schema.respond_to?(:to_hash)
|
244
|
+
nil
|
245
|
+
else
|
246
|
+
if schema.key?('items') || schema.key?('additionalItems')
|
247
|
+
if schema['items'].respond_to?(:to_ary)
|
248
|
+
if schema['items'].each_index.to_a.include?(idx)
|
249
|
+
ptr['items'][idx]
|
250
|
+
elsif schema.key?('additionalItems')
|
251
|
+
ptr['additionalItems']
|
252
|
+
else
|
253
|
+
nil
|
254
|
+
end
|
255
|
+
elsif schema.key?('items')
|
256
|
+
ptr['items']
|
257
|
+
else
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
else
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# given this Pointer points to a schema in the given document, this matches
|
267
|
+
# any oneOf or anyOf subschema of the schema which the given instance validates
|
268
|
+
# against. if a subschema is matched, a pointer to that schema is returned; if not,
|
269
|
+
# self is returned.
|
270
|
+
#
|
271
|
+
# @param document [#to_hash, #to_ary, Object] document containing the schema
|
272
|
+
# this pointer points to
|
273
|
+
# @param instance [Object] the instance to which to attempt to match *Of subschemas
|
274
|
+
# @return [JSI::JSON::Pointer] either a pointer to a *Of subschema in the document,
|
275
|
+
# or self if no other subschema was matched
|
276
|
+
def schema_match_ptr_to_instance(document, instance)
|
277
|
+
ptr = self
|
278
|
+
schema = ptr.evaluate(document)
|
279
|
+
if schema.respond_to?(:to_hash)
|
280
|
+
# matching oneOf is good here. one schema for one instance.
|
281
|
+
# matching anyOf is fine. there could be more than one schema matched but it's usually just
|
282
|
+
# one. if more than one is a match, you just get the first one.
|
283
|
+
someof_token = %w(oneOf anyOf).detect { |k| schema[k].respond_to?(:to_ary) }
|
284
|
+
if someof_token
|
285
|
+
someof_ptr = ptr[someof_token].deref(document)
|
286
|
+
someof_ptr.evaluate(document).each_index do |i|
|
287
|
+
someof_schema_ptr = someof_ptr[i].deref(document)
|
288
|
+
valid = ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: someof_schema_ptr.fragment)
|
289
|
+
if valid
|
290
|
+
return someof_schema_ptr.schema_match_ptr_to_instance(document, instance)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
return ptr
|
296
|
+
end
|
297
|
+
|
198
298
|
# takes a document and a block. the block is yielded the content of the given document at this
|
199
299
|
# pointer's location. the block must result a modified copy of that content (and MUST NOT modify
|
200
300
|
# the object it is given). this modified copy of that content is incorporated into a modified copy
|
@@ -302,13 +402,10 @@ module JSI
|
|
302
402
|
"#<#{self.class.inspect} #{representation_s}>"
|
303
403
|
end
|
304
404
|
|
305
|
-
|
306
|
-
def to_s
|
307
|
-
inspect
|
308
|
-
end
|
405
|
+
alias_method :to_s, :inspect
|
309
406
|
|
310
407
|
# pointers are equal if the reference_tokens are equal, regardless of @type
|
311
|
-
def
|
408
|
+
def jsi_fingerprint
|
312
409
|
{class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
|
313
410
|
end
|
314
411
|
include FingerprintHash
|