Class: Scorpio::JSON::Node

Inherits:
Object
  • Object
show all
Includes:
FingerprintHash
Defined in:
lib/scorpio/json/node.rb

Overview

Scorpio::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 (Hash or Array, generally) in most cases, defining methods of Hash and Array which delegate to the content. However, destructive methods are not defined, as modifying the content of a node would change it for any other nodes in the document that contain or refer to it.

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.

Direct Known Subclasses

ArrayNode, HashNode

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FingerprintHash

#==, #hash

Constructor Details

#initialize(document, path) ⇒ Node

a Node represents the content of a document at a given path.

Raises:

  • (ArgumentError)


31
32
33
34
35
36
# File 'lib/scorpio/json/node.rb', line 31

def initialize(document, path)
  raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
  @document = document
  @path = path.dup.freeze
  @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
end

Instance Attribute Details

#documentObject (readonly)

the document containing this Node at is path



41
42
43
# File 'lib/scorpio/json/node.rb', line 41

def document
  @document
end

#pathObject (readonly)

the path of this Node within its document



39
40
41
# File 'lib/scorpio/json/node.rb', line 39

def path
  @path
end

#pointerObject (readonly)

::JSON::Schema::Pointer representing the path to this node within its document



43
44
45
# File 'lib/scorpio/json/node.rb', line 43

def pointer
  @pointer
end

Class Method Details

.new_by_type(document, path) ⇒ Object

if the content of the document at the given path is a Hash, returns a HashNode; if an Array, returns ArrayNode. otherwise returns a regular Node, though, for the most part this will be called with Hash or Array content.



18
19
20
21
22
23
24
25
26
27
28
# File 'lib/scorpio/json/node.rb', line 18

def self.new_by_type(document, path)
  node = Node.new(document, path)
  content = node.content
  if content.is_a?(Hash)
    HashNode.new(document, path)
  elsif content.is_a?(Array)
    ArrayNode.new(document, path)
  else
    node
  end
end

Instance Method Details

#[](k) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/scorpio/json/node.rb', line 50

def [](k)
  node = self
  content = node.content
  if content.is_a?(Hash) && !content.key?(k)
    node = node.deref
    content = node.content
  end
  begin
    el = content[k]
  rescue TypeError => e
    raise(e.class, e.message + "\nsubscripting with #{k.pretty_inspect.chomp} (#{k.class}) from #{content.class.inspect}. self is: #{pretty_inspect.chomp}", e.backtrace)
  end
  if el.is_a?(Hash) || el.is_a?(Array)
    self.class.new_by_type(node.document, node.path + [k])
  else
    el
  end
end

#as_jsonObject



117
118
119
# File 'lib/scorpio/json/node.rb', line 117

def as_json
  Typelike.as_json(content)
end

#contentObject

the raw content of this Node from the underlying document at this Node’s path.



46
47
48
# File 'lib/scorpio/json/node.rb', line 46

def content
  pointer.evaluate(document)
end

#derefObject



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/scorpio/json/node.rb', line 69

def deref
  content = self.content

  return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)

  if content['$ref'][/\A#/]
    return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
  end

  # HAX for how google does refs and ids
  if document_node['schemas'].respond_to?(:to_hash)
    if document_node['schemas'][content['$ref']]
      return document_node['schemas'][content['$ref']]
    end
    _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
    if deref_by_id
      return deref_by_id
    end
  end

  #raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
  return self
end

#document_nodeObject

a Node at the root of the document



94
95
96
# File 'lib/scorpio/json/node.rb', line 94

def document_node
  Node.new_by_type(document, [])
end

#fingerprintObject



189
190
191
# File 'lib/scorpio/json/node.rb', line 189

def fingerprint
  {is_node: self.is_a?(Scorpio::JSON::Node), document: document, path: path}
end

#fragmentObject

the pointer fragment to this node within the document, per RFC 6901 tools.ietf.org/html/rfc6901



113
114
115
# File 'lib/scorpio/json/node.rb', line 113

def fragment
  pointer.fragment
end

#inspectObject



172
173
174
# File 'lib/scorpio/json/node.rb', line 172

def inspect
  "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
end

#modified_copyObject

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).



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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/scorpio/json/node.rb', line 123

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 = 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)
          car_object = rec.call(subdocument[car], cdr)
          if car_object.object_id == subdocument[car].object_id
            subdocument
          else
            subdocument.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
          car_object = rec.call(subdocument[car], cdr)
          if car_object.object_id == subdocument[car].object_id
            subdocument
          else
            subdocument.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_textObject



169
170
171
# File 'lib/scorpio/json/node.rb', line 169

def object_group_text
  "fragment=#{fragment.inspect}"
end

#parent_nodeObject

the parent of this node. if this node is the document root (its path is empty), raises ::JSON::Schema::Pointer::ReferenceError.



100
101
102
103
104
105
106
# File 'lib/scorpio/json/node.rb', line 100

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_pathObject

the pointer path to this node within the document, per RFC 6901 tools.ietf.org/html/rfc6901



109
110
111
# File 'lib/scorpio/json/node.rb', line 109

def pointer_path
  pointer.pointer
end

#pretty_print(q) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/scorpio/json/node.rb', line 175

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