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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +3 -1
  3. data/CHANGELOG.md +48 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +84 -45
  6. data/jsi.gemspec +11 -14
  7. data/lib/jsi.rb +31 -12
  8. data/lib/jsi/base.rb +310 -344
  9. data/lib/jsi/base/to_rb.rb +2 -0
  10. data/lib/jsi/jsi_coder.rb +91 -0
  11. data/lib/jsi/json-schema-fragments.rb +3 -135
  12. data/lib/jsi/json.rb +3 -0
  13. data/lib/jsi/json/node.rb +72 -197
  14. data/lib/jsi/json/pointer.rb +419 -0
  15. data/lib/jsi/metaschema.rb +7 -0
  16. data/lib/jsi/metaschema_node.rb +218 -0
  17. data/lib/jsi/pathed_node.rb +118 -0
  18. data/lib/jsi/schema.rb +168 -223
  19. data/lib/jsi/schema_classes.rb +158 -0
  20. data/lib/jsi/simple_wrap.rb +12 -0
  21. data/lib/jsi/typelike_modules.rb +71 -45
  22. data/lib/jsi/util.rb +47 -57
  23. data/lib/jsi/version.rb +1 -1
  24. data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
  25. data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
  26. data/resources/icons/AGPL-3.0.png +0 -0
  27. data/test/base_array_test.rb +210 -84
  28. data/test/base_hash_test.rb +201 -58
  29. data/test/base_test.rb +212 -121
  30. data/test/jsi_coder_test.rb +85 -0
  31. data/test/jsi_json_arraynode_test.rb +26 -25
  32. data/test/jsi_json_hashnode_test.rb +40 -39
  33. data/test/jsi_json_node_test.rb +95 -126
  34. data/test/jsi_json_pointer_test.rb +102 -0
  35. data/test/jsi_typelike_as_json_test.rb +53 -0
  36. data/test/metaschema_node_test.rb +19 -0
  37. data/test/schema_module_test.rb +21 -0
  38. data/test/schema_test.rb +109 -97
  39. data/test/spreedly_openapi_test.rb +8 -0
  40. data/test/test_helper.rb +42 -8
  41. data/test/util_test.rb +14 -14
  42. metadata +54 -25
  43. data/LICENSE.txt +0 -21
  44. data/lib/jsi/schema_instance_json_coder.rb +0 -83
  45. data/lib/jsi/struct_json_coder.rb +0 -30
  46. data/test/schema_instance_json_coder_test.rb +0 -121
  47. data/test/struct_json_coder_test.rb +0 -130
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  class Base
3
5
  class << self
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # this is an ActiveRecord serialization coder intended to serialize between
5
+ # JSON-compatible objects on the database side, and a JSI instance loaded on
6
+ # the model attribute.
7
+ #
8
+ # on its own this coder is useful with a JSON database column. in order to
9
+ # serialize further to a string of JSON, or to YAML, the gem `arms` allows
10
+ # coders to be chained together. for example, for a table `foos` and a column
11
+ # `preferences_json` which is an actual json column, and `preferences_txt`
12
+ # which is a string:
13
+ #
14
+ # Preferences = JSI.class_for_schema(preferences_json_schema)
15
+ # class Foo < ActiveRecord::Base
16
+ # # as a single serializer, loads a Preferences instance from a json column
17
+ # serialize 'preferences_json', JSI::JSICoder.new(Preferences)
18
+ #
19
+ # # for a text column, arms_serialize will go from JSI to JSON-compatible
20
+ # # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
21
+ # arms_serialize 'preferences_txt', [:jsi, Preferences], :json
22
+ # end
23
+ #
24
+ # the column data may be either a single instance of the schema class
25
+ # (represented as one json object) or an array of them (represented as a json
26
+ # array of json objects), indicated by the keyword argument `array`.
27
+ class JSICoder
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
37
+ @array = array
38
+ end
39
+
40
+ # loads the database column to JSI instances of our schema
41
+ #
42
+ # @param data [Object, Array, nil] the dumped schema instance(s) of the JSI(s)
43
+ # @return [JSI::Base, Array<JSI::Base>, nil] the JSI or JSIs containing the schema
44
+ # instance(s), or nil if data is nil
45
+ def load(data)
46
+ return nil if data.nil?
47
+ object = if @array
48
+ unless data.respond_to?(:to_ary)
49
+ raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
50
+ end
51
+ data.map { |el| load_object(el) }
52
+ else
53
+ load_object(data)
54
+ end
55
+ object
56
+ end
57
+
58
+ # @param object [JSI::Base, Array<JSI::Base>, nil] the JSI or array of JSIs containing
59
+ # the schema instance(s)
60
+ # @return [Object, Array, nil] the schema instance(s) of the JSI(s), or nil if object is nil
61
+ def dump(object)
62
+ return nil if object.nil?
63
+ jsonifiable = begin
64
+ if @array
65
+ unless object.respond_to?(:to_ary)
66
+ raise(TypeError, "expected array-like attribute; got: #{object.class}: #{object.inspect}")
67
+ end
68
+ object.map do |el|
69
+ dump_object(el)
70
+ end
71
+ else
72
+ dump_object(object)
73
+ end
74
+ end
75
+ jsonifiable
76
+ end
77
+
78
+ private
79
+ # @param data [Object]
80
+ # @return [JSI::Base]
81
+ def load_object(data)
82
+ @schema.new_jsi(data)
83
+ end
84
+
85
+ # @param object [JSI::Base, Object]
86
+ # @return [Object]
87
+ def dump_object(object)
88
+ JSI::Typelike.as_json(object)
89
+ end
90
+ end
91
+ end
@@ -1,141 +1,9 @@
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
4
6
 
5
- # json-schema/pointer.rb
6
- require 'addressable/uri'
7
-
8
- module JSON
9
- class Schema
10
- # a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
11
- class Pointer
12
- class Error < JSON::Schema::SchemaError
13
- end
14
- class PointerSyntaxError < Error
15
- end
16
- class ReferenceError < Error
17
- end
18
-
19
- # parse a fragment to an array of reference tokens
20
- #
21
- # #/foo/bar
22
- #
23
- # => ['foo', 'bar']
24
- #
25
- # #/foo%20bar
26
- #
27
- # => ['foo bar']
28
- def self.parse_fragment(fragment)
29
- fragment = Addressable::URI.unescape(fragment)
30
- match = fragment.match(/\A#/)
31
- if match
32
- parse_pointer(match.post_match)
33
- else
34
- raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
35
- end
36
- end
37
-
38
- # parse a pointer to an array of reference tokens
39
- #
40
- # /foo
41
- #
42
- # => ['foo']
43
- #
44
- # /foo~0bar/baz~1qux
45
- #
46
- # => ['foo~bar', 'baz/qux']
47
- def self.parse_pointer(pointer_string)
48
- tokens = pointer_string.split('/', -1).map! do |piece|
49
- piece.gsub('~1', '/').gsub('~0', '~')
50
- end
51
- if tokens[0] == ''
52
- tokens[1..-1]
53
- elsif tokens.empty?
54
- tokens
55
- else
56
- raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
57
- end
58
- end
59
-
60
- # initializes a JSON::Schema::Pointer from the given representation.
61
- #
62
- # type may be one of:
63
- #
64
- # - :fragment - the representation is a fragment containing a pointer (starting with #)
65
- # - :pointer - the representation is a pointer (starting with /)
66
- # - :reference_tokens - the representation is an array of tokens referencing a path in a document
67
- def initialize(type, representation)
68
- @type = type
69
- if type == :reference_tokens
70
- reference_tokens = representation
71
- elsif type == :fragment
72
- reference_tokens = self.class.parse_fragment(representation)
73
- elsif type == :pointer
74
- reference_tokens = self.class.parse_pointer(representation)
75
- else
76
- raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
77
- end
78
- @reference_tokens = reference_tokens.map(&:freeze).freeze
79
- end
80
-
81
- attr_reader :reference_tokens
82
-
83
- # takes a root json document and evaluates this pointer through the document, returning the value
84
- # pointed to by this pointer.
85
- def evaluate(document)
86
- res = reference_tokens.inject(document) do |value, token|
87
- if value.respond_to?(:to_ary)
88
- if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
89
- token = token.to_i
90
- end
91
- unless token.is_a?(Integer)
92
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
93
- end
94
- unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
95
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
96
- end
97
- (value.respond_to?(:[]) ? value : value.to_ary)[token]
98
- elsif value.respond_to?(:to_hash)
99
- unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
100
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
101
- end
102
- (value.respond_to?(:[]) ? value : value.to_hash)[token]
103
- else
104
- raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
105
- end
106
- end
107
- res
108
- end
109
-
110
- # the pointer string representation of this Pointer
111
- def pointer
112
- reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
113
- end
114
-
115
- # the fragment string representation of this Pointer
116
- def fragment
117
- '#' + Addressable::URI.escape(pointer)
118
- end
119
-
120
- def to_s
121
- "#<#{self.class.inspect} #{@type} = #{representation_s}>"
122
- end
123
-
124
- private
125
-
126
- def representation_s
127
- if @type == :fragment
128
- fragment
129
- elsif @type == :pointer
130
- pointer
131
- else
132
- reference_tokens.inspect
133
- end
134
- end
135
- end
136
- end
137
- end
138
-
139
7
  # json-schema/validator.rb
140
8
 
141
9
  module JSON
@@ -177,7 +45,7 @@ module JSON
177
45
  def schema_from_fragment(base_schema, fragment)
178
46
  schema_uri = base_schema.uri
179
47
 
180
- pointer = JSON::Schema::Pointer.new(:fragment, fragment)
48
+ pointer = JSI::JSON::Pointer.from_fragment(fragment)
181
49
 
182
50
  base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
183
51
 
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  module JSON
3
5
  autoload :Node, 'jsi/json/node'
4
6
  autoload :ArrayNode, 'jsi/json/node'
5
7
  autoload :HashNode, 'jsi/json/node'
8
+ autoload :Pointer, 'jsi/json/pointer'
6
9
  end
7
10
  end
@@ -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,51 +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
25
- def self.new_doc(document)
26
- new_by_type(document, [])
27
+ include Enumerable
28
+ include PathedNode
29
+
30
+ def self.new_doc(node_document)
31
+ new_by_type(node_document, JSI::JSON::Pointer.new([]))
27
32
  end
28
33
 
29
- # if the content of the document at the given path is Hash-like, returns
34
+ # if the content of the document at the given pointer is Hash-like, returns
30
35
  # a HashNode; if Array-like, returns ArrayNode. otherwise returns a
31
36
  # regular Node, although Nodes are for the most part instantiated from
32
37
  # Hash or Array-like content.
33
- def self.new_by_type(document, path)
34
- node = Node.new(document, path)
35
- content = node.content
38
+ def self.new_by_type(node_document, node_ptr)
39
+ content = node_ptr.evaluate(node_document)
36
40
  if content.respond_to?(:to_hash)
37
- HashNode.new(document, path)
41
+ HashNode.new(node_document, node_ptr)
38
42
  elsif content.respond_to?(:to_ary)
39
- ArrayNode.new(document, path)
43
+ ArrayNode.new(node_document, node_ptr)
40
44
  else
41
- node
45
+ Node.new(node_document, node_ptr)
42
46
  end
43
47
  end
44
48
 
45
- # a Node represents the content of a document at a given path.
46
- def initialize(document, path)
47
- unless path.respond_to?(:to_ary)
48
- raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})")
49
+ # a Node represents the content of a document at a given pointer.
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})")
49
53
  end
50
- if document.is_a?(JSI::JSON::Node)
51
- 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}")
52
56
  end
53
- @document = document
54
- @path = path.to_ary.dup.freeze
55
- @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
57
+ @node_document = node_document
58
+ @node_ptr = node_ptr
56
59
  end
57
60
 
58
- # the path of this Node within its document
59
- attr_reader :path
60
- # the document containing this Node at is path
61
- attr_reader :document
62
- # ::JSON::Schema::Pointer representing the path to this node within its document
63
- attr_reader :pointer
61
+ # the document containing this Node at our pointer
62
+ attr_reader :node_document
64
63
 
65
- # the raw content of this Node from the underlying document at this Node's path.
66
- def content
67
- content = pointer.evaluate(document)
68
- content
69
- end
64
+ # JSI::JSON::Pointer pointing to this node within its document
65
+ attr_reader :node_ptr
70
66
 
71
67
  # returns content at the given subscript - call this the subcontent.
72
68
  #
@@ -78,12 +74,8 @@ module JSI
78
74
  # if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
79
75
  # not a key of the hash, then the $ref is followed before returning the subcontent.
80
76
  def [](subscript)
81
- node = self
82
- content = node.content
83
- if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
84
- node = node.deref
85
- content = node.content
86
- end
77
+ ptr = self.node_ptr
78
+ content = self.node_content
87
79
  unless content.respond_to?(:[])
88
80
  if content.respond_to?(:to_hash)
89
81
  content = content.to_hash
@@ -99,9 +91,9 @@ module JSI
99
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)
100
92
  end
101
93
  if subcontent.respond_to?(:to_hash)
102
- HashNode.new(node.document, node.path + [subscript])
94
+ HashNode.new(node_document, ptr[subscript])
103
95
  elsif subcontent.respond_to?(:to_ary)
104
- ArrayNode.new(node.document, node.path + [subscript])
96
+ ArrayNode.new(node_document, ptr[subscript])
105
97
  else
106
98
  subcontent
107
99
  end
@@ -110,9 +102,9 @@ module JSI
110
102
  # assigns the given subscript of the content to the given value. the document is modified in place.
111
103
  def []=(subscript, value)
112
104
  if value.is_a?(Node)
113
- content[subscript] = value.content
105
+ node_content[subscript] = value.node_content
114
106
  else
115
- content[subscript] = value
107
+ node_content[subscript] = value
116
108
  end
117
109
  end
118
110
 
@@ -120,209 +112,92 @@ module JSI
120
112
  # does not have a $ref, or if what its $ref cannot be found, this node is returned.
121
113
  #
122
114
  # currently only $refs pointing within the same document are followed.
123
- def deref
124
- content = self.content
125
-
126
- if content.respond_to?(:to_hash)
127
- ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
128
- end
129
- return self unless ref.is_a?(String)
130
-
131
- if ref[/\A#/]
132
- return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref
133
- end
134
-
135
- # HAX for how google does refs and ids
136
- if document_node['schemas'].respond_to?(:to_hash)
137
- if document_node['schemas'][ref]
138
- return document_node['schemas'][ref]
139
- end
140
- _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
141
- if deref_by_id
142
- return deref_by_id
143
- end
115
+ #
116
+ # @yield [Node] if a block is given (optional), this will yield a deref'd node. if this
117
+ # node is not a $ref object, the block is not called. if we are a $ref which cannot be followed
118
+ # (e.g. a $ref to an external document, which is not yet supported), the block is not called.
119
+ # @return [JSI::JSON::Node] dereferenced node, or this node
120
+ def deref(&block)
121
+ node_ptr_deref do |deref_ptr|
122
+ return Node.new_by_type(node_document, deref_ptr).tap(&(block || Util::NOOP))
144
123
  end
145
-
146
- #raise(NotImplementedError, "cannot dereference #{ref}") # TODO
147
124
  return self
148
125
  end
149
126
 
150
127
  # a Node at the root of the document
151
- def document_node
152
- Node.new_doc(document)
128
+ def document_root_node
129
+ Node.new_doc(node_document)
153
130
  end
154
131
 
155
- # the parent of this node. if this node is the document root (its path is empty), raises
156
- # ::JSON::Schema::Pointer::ReferenceError.
132
+ # the parent of this node. if this node is the document root, raises
133
+ # JSI::JSON::Pointer::ReferenceError.
157
134
  def parent_node
158
- if path.empty?
159
- raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
160
- else
161
- Node.new_by_type(document, path[0...-1])
162
- end
163
- end
164
-
165
- # the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
166
- def pointer_path
167
- pointer.pointer
168
- end
169
-
170
- # the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
171
- def fragment
172
- pointer.fragment
135
+ Node.new_by_type(node_document, node_ptr.parent)
173
136
  end
174
137
 
175
138
  # returns a jsonifiable representation of this node's content
176
139
  def as_json(*opt)
177
- Typelike.as_json(content, *opt)
140
+ Typelike.as_json(node_content, *opt)
178
141
  end
179
142
 
180
143
  # takes a block. the block is yielded the content of this node. the block MUST return a modified
181
144
  # copy of that content (and NOT modify the object it is given).
182
- def modified_copy
183
- # we need to preserve the rest of the document, but modify the content at our path.
184
- #
185
- # this is actually a bit tricky. we can't modify the original document, obviously.
186
- # we could do a deep copy, but that's expensive. instead, we make a copy of each array
187
- # or hash in the path above this node. this node's content is modified by the caller, and
188
- # that is recursively merged up to the document root. the recursion is done with a
189
- # y combinator, for no other reason than that was a fun way to implement it.
190
- modified_document = JSI::Util.ycomb do |rec|
191
- proc do |subdocument, subpath|
192
- if subpath == []
193
- yield(subdocument)
194
- else
195
- car = subpath[0]
196
- cdr = subpath[1..-1]
197
- if subdocument.respond_to?(:to_hash)
198
- subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
199
- car_object = rec.call(subdocument_car, cdr)
200
- if car_object.object_id == subdocument_car.object_id
201
- subdocument
202
- else
203
- (subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
204
- end
205
- elsif subdocument.respond_to?(:to_ary)
206
- if car.is_a?(String) && car =~ /\A\d+\z/
207
- car = car.to_i
208
- end
209
- unless car.is_a?(Integer)
210
- raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
211
- end
212
- subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
213
- car_object = rec.call(subdocument_car, cdr)
214
- if car_object.object_id == subdocument_car.object_id
215
- subdocument
216
- else
217
- (subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
218
- arr[car] = car_object
219
- end
220
- end
221
- else
222
- raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
223
- end
224
- end
225
- end
226
- end.call(document, path)
227
- Node.new_by_type(modified_document, path)
145
+ def modified_copy(&block)
146
+ Node.new_by_type(node_ptr.modified_document_copy(node_document, &block), node_ptr)
147
+ end
148
+
149
+ def dup
150
+ modified_copy(&:dup)
228
151
  end
229
152
 
230
153
  # meta-information about the object, outside the content. used by #inspect / #pretty_print
154
+ # @return [Array<String>]
231
155
  def object_group_text
232
- "fragment=#{fragment.inspect}" + (content.respond_to?(:object_group_text) ? ' ' + content.object_group_text : '')
156
+ [
157
+ self.class.inspect,
158
+ node_ptr.uri.to_s,
159
+ ] + (node_content.respond_to?(:object_group_text) ? node_content.object_group_text : [])
233
160
  end
234
161
 
235
162
  # a string representing this node
236
163
  def inspect
237
- "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
164
+ "\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
238
165
  end
239
166
 
240
167
  # pretty-prints a representation this node to the given printer
241
168
  def pretty_print(q)
242
- q.instance_exec(self) do |obj|
243
- text "\#<#{obj.class.inspect} #{obj.object_group_text}"
244
- group_sub {
245
- nest(2) {
246
- breakable ' '
247
- pp obj.content
248
- }
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
249
175
  }
250
- breakable ''
251
- text '>'
252
- end
176
+ }
177
+ q.breakable ''
178
+ q.text '>'
253
179
  end
254
180
 
255
181
  # fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
256
182
  # (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
257
- # documents at equal paths. note that this means two nodes with the same content may not be
183
+ # documents at equal pointers. note that this means two nodes with the same content may not be
258
184
  # considered equal.
259
- def fingerprint
260
- {is_node: self.is_a?(JSI::JSON::Node), document: document, path: path}
185
+ def jsi_fingerprint
186
+ {class: JSI::JSON::Node, node_document: node_document, node_ptr: node_ptr}
261
187
  end
262
- include FingerprintHash
188
+ include Util::FingerprintHash
263
189
  end
264
190
 
265
191
  # a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
266
192
  # and includes Array methods from Arraylike
267
193
  class ArrayNode < Node
268
- # iterates over each element in the same manner as Array#each
269
- def each
270
- return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
271
- (content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
272
- self
273
- end
274
-
275
- # the content of this ArrayNode, as an Array
276
- def to_ary
277
- to_a
278
- end
279
-
280
- include Enumerable
281
- include Arraylike
282
-
283
- # returns a jsonifiable representation of this node's content
284
- def as_json(*opt) # needs redefined after including Enumerable
285
- Typelike.as_json(content, *opt)
286
- end
287
-
288
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
289
- # we override these methods from Arraylike
290
- SAFE_INDEX_ONLY_METHODS.each do |method_name|
291
- define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
292
- end
194
+ include PathedArrayNode
293
195
  end
294
196
 
295
197
  # a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
296
198
  # and includes Hash methods from Hashlike
297
199
  class HashNode < Node
298
- # iterates over each element in the same manner as Array#each
299
- def each(&block)
300
- return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
301
- if block.arity > 1
302
- (content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
303
- else
304
- (content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
305
- end
306
- self
307
- end
308
-
309
- # the content of this HashNode, as a Hash
310
- def to_hash
311
- inject({}) { |h, (k, v)| h[k] = v; h }
312
- end
313
-
314
- include Enumerable
315
- include Hashlike
316
-
317
- # returns a jsonifiable representation of this node's content
318
- def as_json(*opt) # needs redefined after including Enumerable
319
- Typelike.as_json(content, *opt)
320
- end
321
-
322
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
323
- SAFE_KEY_ONLY_METHODS.each do |method_name|
324
- define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
325
- end
200
+ include PathedHashNode
326
201
  end
327
202
  end
328
203
  end