jsi 0.0.4 → 0.4.0

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