jsi 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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