Class: JSI::JSON::Node
- Inherits:
-
Object
- Object
- JSI::JSON::Node
- Includes:
- FingerprintHash
- Defined in:
- lib/jsi/json/node.rb
Overview
JSI::JSON::Node is an abstraction of a node within a JSON document. it aims to act like the underlying data type of the node's content (generally Hash or Array-like) in most cases.
the main advantage offered by using a Node over the underlying data is in dereferencing. if a Node consists of a hash with a $ref property pointing within the same document, then the Node will transparently follow the ref and return the referenced data.
in most other respects, a Node aims to act like a Hash when the content is Hash-like, an Array when the content is Array-like. methods of Hash and Array are defined and delegated to the node's content.
however, destructive methods are for the most part not implemented. at the moment only #[]= is implemented. since Node thinly wraps the underlying data, you can change the data and it will be reflected in the node. implementations of destructive methods are planned.
methods that return a modified copy such as #merge are defined, and return a copy of the document with the content of the node modified. the original node's document and content are untouched.
Instance Attribute Summary collapse
-
#document ⇒ Object
readonly
the document containing this Node at is path.
-
#path ⇒ Object
readonly
the path of this Node within its document.
-
#pointer ⇒ Object
readonly
::JSON::Schema::Pointer representing the path to this node within its document.
Class Method Summary collapse
-
.new_by_type(document, path) ⇒ Object
if the content of the document at the given path is Hash-like, returns a HashNode; if Array-like, returns ArrayNode.
- .new_doc(document) ⇒ Object
Instance Method Summary collapse
-
#[](subscript) ⇒ Object
returns content at the given subscript - call this the subcontent.
-
#[]=(subscript, value) ⇒ Object
assigns the given subscript of the content to the given value.
-
#as_json(*opt) ⇒ Object
returns a jsonifiable representation of this node's content.
-
#content ⇒ Object
the raw content of this Node from the underlying document at this Node's path.
-
#deref ⇒ Object
returns a Node, dereferencing a $ref attribute if possible.
-
#document_node ⇒ Object
a Node at the root of the document.
-
#fingerprint ⇒ Object
fingerprint for equality (see FingerprintHash).
-
#fragment ⇒ Object
the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901.
-
#initialize(document, path) ⇒ Node
constructor
a Node represents the content of a document at a given path.
-
#inspect ⇒ Object
a string representing this node.
-
#modified_copy ⇒ Object
takes a block.
-
#object_group_text ⇒ Object
meta-information about the object, outside the content.
-
#parent_node ⇒ Object
the parent of this node.
-
#pointer_path ⇒ Object
the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901.
-
#pretty_print(q) ⇒ Object
pretty-prints a representation this node to the given printer.
Methods included from FingerprintHash
Constructor Details
#initialize(document, path) ⇒ Node
a Node represents the content of a document at a given path.
46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/jsi/json/node.rb', line 46 def initialize(document, path) unless path.respond_to?(:to_ary) raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") end if document.is_a?(JSI::JSON::Node) raise(TypeError, "document of a Node should not be another JSI::JSON::Node: #{document.inspect}") end @document = document @path = path.to_ary.dup.freeze @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path) end |
Instance Attribute Details
#document ⇒ Object (readonly)
the document containing this Node at is path
61 62 63 |
# File 'lib/jsi/json/node.rb', line 61 def document @document end |
#path ⇒ Object (readonly)
the path of this Node within its document
59 60 61 |
# File 'lib/jsi/json/node.rb', line 59 def path @path end |
#pointer ⇒ Object (readonly)
::JSON::Schema::Pointer representing the path to this node within its document
63 64 65 |
# File 'lib/jsi/json/node.rb', line 63 def pointer @pointer end |
Class Method Details
.new_by_type(document, path) ⇒ Object
if the content of the document at the given path is Hash-like, returns a HashNode; if Array-like, returns ArrayNode. otherwise returns a regular Node, although Nodes are for the most part instantiated from Hash or Array-like content.
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/jsi/json/node.rb', line 33 def self.new_by_type(document, path) node = Node.new(document, path) content = node.content if content.respond_to?(:to_hash) HashNode.new(document, path) elsif content.respond_to?(:to_ary) ArrayNode.new(document, path) else node end end |
.new_doc(document) ⇒ Object
25 26 27 |
# File 'lib/jsi/json/node.rb', line 25 def self.new_doc(document) new_by_type(document, []) end |
Instance Method Details
#[](subscript) ⇒ Object
returns content at the given subscript - call this the subcontent.
if the content cannot be subscripted, raises TypeError.
if the subcontent is Hash-like, it is wrapped as a JSI::JSON::HashNode before being returned. if the subcontent is Array-like, it is wrapped as a JSI::JSON::ArrayNode before being returned.
if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is not a key of the hash, then the $ref is followed before returning the subcontent.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/jsi/json/node.rb', line 79 def [](subscript) node = self content = node.content if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript) node = node.deref content = node.content end unless content.respond_to?(:[]) if content.respond_to?(:to_hash) content = content.to_hash elsif content.respond_to?(:to_ary) content = content.to_ary else raise(NoMethodError, "undefined method `[]`\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}") end end begin subcontent = content[subscript] rescue TypeError => e raise(e.class, e. + "\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}", e.backtrace) end if subcontent.respond_to?(:to_hash) HashNode.new(node.document, node.path + [subscript]) elsif subcontent.respond_to?(:to_ary) ArrayNode.new(node.document, node.path + [subscript]) else subcontent end end |
#[]=(subscript, value) ⇒ Object
assigns the given subscript of the content to the given value. the document is modified in place.
110 111 112 113 114 115 116 |
# File 'lib/jsi/json/node.rb', line 110 def []=(subscript, value) if value.is_a?(Node) content[subscript] = value.content else content[subscript] = value end end |
#as_json(*opt) ⇒ Object
returns a jsonifiable representation of this node's content
175 176 177 |
# File 'lib/jsi/json/node.rb', line 175 def as_json(*opt) Typelike.as_json(content, *opt) end |
#content ⇒ Object
the raw content of this Node from the underlying document at this Node's path.
66 67 68 |
# File 'lib/jsi/json/node.rb', line 66 def content pointer.evaluate(document) end |
#deref ⇒ Object
returns a Node, dereferencing a $ref attribute if possible. if this node is not hash-like, does not have a $ref, or if what its $ref cannot be found, this node is returned.
currently only $refs pointing within the same document are followed.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/jsi/json/node.rb', line 122 def deref content = self.content if content.respond_to?(:to_hash) ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref'] end return self unless ref.is_a?(String) if ref[/\A#/] return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref end # HAX for how google does refs and ids if document_node['schemas'].respond_to?(:to_hash) if document_node['schemas'][ref] return document_node['schemas'][ref] end _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref } if deref_by_id return deref_by_id end end #raise(NotImplementedError, "cannot dereference #{ref}") # TODO return self end |
#document_node ⇒ Object
a Node at the root of the document
150 151 152 |
# File 'lib/jsi/json/node.rb', line 150 def document_node Node.new_doc(document) end |
#fingerprint ⇒ Object
fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal documents at equal paths. note that this means two nodes with the same content may not be considered equal.
258 259 260 |
# File 'lib/jsi/json/node.rb', line 258 def fingerprint {is_node: self.is_a?(JSI::JSON::Node), document: document, path: path} end |
#fragment ⇒ Object
the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
170 171 172 |
# File 'lib/jsi/json/node.rb', line 170 def fragment pointer.fragment end |
#inspect ⇒ Object
a string representing this node
235 236 237 |
# File 'lib/jsi/json/node.rb', line 235 def inspect "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>" end |
#modified_copy ⇒ Object
takes a block. the block is yielded the content of this node. the block MUST return a modified copy of that content (and NOT modify the object it is given).
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/jsi/json/node.rb', line 181 def modified_copy # we need to preserve the rest of the document, but modify the content at our path. # # this is actually a bit tricky. we can't modify the original document, obviously. # we could do a deep copy, but that's expensive. instead, we make a copy of each array # or hash in the path above this node. this node's content is modified by the caller, and # that is recursively merged up to the document root. the recursion is done with a # y combinator, for no other reason than that was a fun way to implement it. modified_document = JSI::Util.ycomb do |rec| proc do |subdocument, subpath| if subpath == [] yield(subdocument) else car = subpath[0] cdr = subpath[1..-1] if subdocument.respond_to?(:to_hash) subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car] car_object = rec.call(subdocument_car, cdr) if car_object.object_id == subdocument_car.object_id subdocument else (subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object}) end elsif subdocument.respond_to?(:to_ary) if car.is_a?(String) && car =~ /\A\d+\z/ car = car.to_i end unless car.is_a?(Integer) raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}") end subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car] car_object = rec.call(subdocument_car, cdr) if car_object.object_id == subdocument_car.object_id subdocument else (subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr| arr[car] = car_object end end else raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}") end end end end.call(document, path) Node.new_by_type(modified_document, path) end |
#object_group_text ⇒ Object
meta-information about the object, outside the content. used by #inspect / #pretty_print
230 231 232 |
# File 'lib/jsi/json/node.rb', line 230 def object_group_text "fragment=#{fragment.inspect}" + (content.respond_to?(:object_group_text) ? ' ' + content.object_group_text : '') end |
#parent_node ⇒ Object
the parent of this node. if this node is the document root (its path is empty), raises ::JSON::Schema::Pointer::ReferenceError.
156 157 158 159 160 161 162 |
# File 'lib/jsi/json/node.rb', line 156 def parent_node if path.empty? raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}") else Node.new_by_type(document, path[0...-1]) end end |
#pointer_path ⇒ Object
the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
165 166 167 |
# File 'lib/jsi/json/node.rb', line 165 def pointer_path pointer.pointer end |
#pretty_print(q) ⇒ Object
pretty-prints a representation this node to the given printer
240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/jsi/json/node.rb', line 240 def pretty_print(q) q.instance_exec(self) do |obj| text "\#<#{obj.class.inspect} #{obj.object_group_text}" group_sub { nest(2) { breakable ' ' pp obj.content } } breakable '' text '>' end end |