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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  class Base
3
5
  class << self
@@ -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 loaded class
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 loaded_class [Class] the JSI::Base subclass which #load will instantiate
27
- # @param array [Boolean] whether the dumped data represent one instance of loaded_class,
28
- # or an array of them. note that it may be preferable to have loaded_class simply be
29
- # an array schema class.
30
- def initialize(loaded_class, array: false)
31
- @loaded_class = loaded_class
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 #loaded_class
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 [loaded_class instance, Array<loaded_class instance>, nil] the JSI or JSIs
39
- # containing the schema instance(s), or nil if data is nil
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 [loaded_class instance, Array<loaded_class instance>, nil] the JSI or array
54
- # of JSIs containing the schema instance(s)
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 [loaded_class]
80
+ # @return [JSI::Base]
76
81
  def load_object(data)
77
- @loaded_class.new(data)
82
+ @schema.new_jsi(data)
78
83
  end
79
84
 
80
- # @param object [loaded_class]
85
+ # @param object [JSI::Base, Object]
81
86
  # @return [Object]
82
87
  def dump_object(object)
83
88
  JSI::Typelike.as_json(object)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json-schema"
2
4
 
3
5
  # apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  module JSON
3
5
  autoload :Node, 'jsi/json/node'
@@ -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(document)
28
- new_by_type(document, JSI::JSON::Pointer.new([]))
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(document, pointer)
36
- content = pointer.evaluate(document)
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(document, pointer)
41
+ HashNode.new(node_document, node_ptr)
39
42
  elsif content.respond_to?(:to_ary)
40
- ArrayNode.new(document, pointer)
43
+ ArrayNode.new(node_document, node_ptr)
41
44
  else
42
- Node.new(document, pointer)
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(document, pointer)
48
- unless pointer.is_a?(JSI::JSON::Pointer)
49
- raise(TypeError, "pointer must be a JSI::JSON::Pointer. got: #{pointer.pretty_inspect.chomp} (#{pointer.class})")
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 document.is_a?(JSI::JSON::Node)
52
- raise(TypeError, "document of a Node should not be another JSI::JSON::Node: #{document.inspect}")
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
- @document = document
55
- @pointer = pointer
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 :document
62
+ attr_reader :node_document
60
63
 
61
64
  # JSI::JSON::Pointer pointing to this node within its document
62
- attr_reader :pointer
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.pointer
86
- content = self.content
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(document, ptr[subscript])
94
+ HashNode.new(node_document, ptr[subscript])
109
95
  elsif subcontent.respond_to?(:to_ary)
110
- ArrayNode.new(document, ptr[subscript])
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
- content[subscript] = value.content
105
+ node_content[subscript] = value.node_content
120
106
  else
121
- content[subscript] = value
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
- pointer.deref(document) do |deref_ptr|
136
- return Node.new_by_type(document, deref_ptr).tap(&(block || Util::NOOP))
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 document_node
143
- Node.new_doc(document)
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(document, pointer.parent)
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(content, *opt)
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(pointer.modified_document_copy(document, &block), pointer)
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
- "\#<#{self.class.inspect}#{JSI.object_group_str(object_group_text)} #{node_content.inspect}>"
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.instance_exec(self) do |obj|
200
- text "\#<#{obj.class.inspect}#{JSI.object_group_str(obj.object_group_text)}"
201
- group_sub {
202
- nest(2) {
203
- breakable ' '
204
- pp obj.content
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
- breakable ''
208
- text '>'
209
- end
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 fingerprint
217
- {class: JSI::JSON::Node, document: document, pointer: pointer}
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
@@ -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
- # @return [String] string representation of this Pointer
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 fingerprint
408
+ def jsi_fingerprint
312
409
  {class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
313
410
  end
314
411
  include FingerprintHash