Class: LazyGraph::Node

Inherits:
Object
  • Object
show all
Includes:
DerivedRules
Defined in:
lib/lazy_graph/node.rb,
lib/lazy_graph/node/derived_rules.rb

Overview

Class: Node Represents A single Node within our LazyGraph structure A node is a logical position with a graph structure. The node might capture knowledge about how to derived values at its position if a value is not provided. This can be in the form of a default value or a derivation rule.

This class is heavily optimized to resolve values in a graph structure with as little overhead as possible. (Note heavy use of ivars, and minimal method calls in the recursive resolve method).

Nodes support (non-circular) recursive resolution of values, i.e. if a node depends on the output of several other nodes in the graph, it will resolve those nodes first before resolving itself.

Node resolution maintains a full stack, so that values can be resolved relative to the position of the node itself.

Direct Known Subclasses

ArrayNode, ObjectNode

Defined Under Namespace

Modules: DerivedRules

Constant Summary

Constants included from DerivedRules

DerivedRules::PLACEHOLDER_VAR_REGEX

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from DerivedRules

#build_derived_inputs, #create_derived_input_context, #extract_derived_src, extract_expr_from_source_location, #interpret_derived_proc, #map_derived_inputs_to_paths, #parse_args_with_conditions, #parse_derived_inputs, #parse_rule_string

Constructor Details

#initialize(name, path, node, parent, debug: false, helpers: nil) ⇒ Node

Returns a new instance of Node.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/lazy_graph/node.rb', line 44

def initialize(name, path, node, parent, debug: false, helpers: nil)
  @name = name
  @path = path
  @parent = parent
  @debug = debug
  @depth = parent ? parent.depth + 1 : 0
  @root = parent ? parent.root : self
  @type = node[:type]
  @invisible = debug ? false : node[:invisible]
  @visited = {}.compare_by_identity
  instance_variable_set("@is_#{@type}", true)
  define_singleton_method(:cast, build_caster)
  define_missing_value_proc!

  @has_default = node.key?(:default)
  @default = @has_default ? cast(node[:default]) : MissingValue { @name }
  @resolution_stack = []

  build_derived_inputs(node[:rule], helpers) if node[:rule]
end

Instance Attribute Details

#childrenObject

Returns the value of attribute children.



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

def children
  @children
end

#depthObject

Returns the value of attribute depth.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def depth
  @depth
end

#derivedObject

Returns the value of attribute derived.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def derived
  @derived
end

#invisibleObject

Returns the value of attribute invisible.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def invisible
  @invisible
end

#is_objectObject (readonly)

Returns the value of attribute is_object.



42
43
44
# File 'lib/lazy_graph/node.rb', line 42

def is_object
  @is_object
end

#nameObject

Returns the value of attribute name.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def name
  @name
end

#parentObject

Returns the value of attribute parent.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def parent
  @parent
end

#pathObject

Returns the value of attribute path.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def path
  @path
end

#rootObject

Returns the value of attribute root.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def root
  @root
end

#typeObject

Returns the value of attribute type.



40
41
42
# File 'lib/lazy_graph/node.rb', line 40

def type
  @type
end

Instance Method Details

#absolute_pathObject



152
153
154
155
156
157
158
159
160
161
162
# File 'lib/lazy_graph/node.rb', line 152

def absolute_path
  @absolute_path ||= begin
    next_node = self
    path = []
    while next_node
      path << next_node.name
      next_node = next_node.parent
    end
    path.reverse.join('.')
  end
end

#ancestorsObject



195
196
197
# File 'lib/lazy_graph/node.rb', line 195

def ancestors
  @ancestors ||= [self, *(@parent ? @parent.ancestors : [])]
end

#clear_visits!Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/lazy_graph/node.rb', line 101

def clear_visits!
  @visited.clear
  @resolution_stack.clear
  @path_cache = {}.clear
  @resolvers = {}.clear

  return unless @children
  return @children.clear_visits! if @children.is_a?(Node)

  @children[:properties]&.each_value(&:clear_visits!)
  @children[:pattern_properties]&.each do |(_, node)|
    node.clear_visits!
  end
end

#copy_item!(input, key, stack, path, _i, segment_indexes) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/lazy_graph/node.rb', line 227

def copy_item!(input, key, stack, (path, _i, segment_indexes))
  if segment_indexes
    missing_value = nil
    parts = path.parts.dup
    parts_identity = path.identity
    segment_indexes.each do |index|
      part = resolve_input(stack, parts[index].options.first, key)
      break missing_value = part if part.is_a?(MissingValue)

      part_sym = part.to_s.to_sym
      parts_identity ^= part_sym.object_id << index
      parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
    end
    path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
  end

  result = missing_value || cast(resolve_input(stack, path, key))

  if @debug
    stack.log_debug(
      output: :"#{stack}.#{key}",
      result: result,
      inputs: @node_context.to_h.except(:itself, :stack_ptr),
      calc: @src
    )
  end
  input[key] = result.nil? ? MissingValue { key } : result
end

#define_missing_value_proc!Object



65
66
67
68
69
70
# File 'lib/lazy_graph/node.rb', line 65

def define_missing_value_proc!
  define_singleton_method(
    :MissingValue,
    @debug ? ->(&blk) { MissingValue.new(blk&.call || absolute_path) } : -> { MissingValue::BLANK }
  )
end

#derive_item!(input, key, stack) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/lazy_graph/node.rb', line 256

def derive_item!(input, key, stack)
  @inputs.each do |path, i, segment_indexes|
    if segment_indexes
      missing_value = nil
      parts = path.parts.dup
      parts_identity = path.identity
      segment_indexes.each do |index|
        part = resolve_input(stack, parts[index].options.first, key)
        break missing_value = part if part.is_a?(MissingValue)

        part_sym = part.to_s.to_sym
        parts_identity ^= part_sym.object_id << (index * 8)
        parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
      end
      path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
    end
    result = missing_value || resolve_input(stack, path, key)
    @node_context[i] = result.is_a?(MissingValue) ? nil : result
  end

  @node_context[:itself] = input
  @node_context[:stack_ptr] = stack

  conditions_passed = !(@conditions&.any? do |field, allowed_value|
    allowed_value.is_a?(Array) ? !allowed_value.include?(@node_context[field]) : allowed_value != @node_context[field]
  end)

  ex = nil
  result = \
    if conditions_passed
      output = begin
        cast(@fixed_result || @node_context.process!)
      rescue LazyGraph::AbortError => e
        raise e
      rescue StandardError => e
        ex = e
        LazyGraph.logger.error(e)
        LazyGraph.logger.error(e.backtrace.join("\n"))
        MissingValue { "#{key} raised exception: #{e.message}" }
      end

      input[key] = output.nil? ? MissingValue { key } : output
    else
      MissingValue { key }
    end

  if @debug
    stack.log_debug(
      output: :"#{stack}.#{key}",
      result: HashUtils.deep_dup(result),
      inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
      calc: @src,
      **(@conditions ? { conditions: @conditions } : {}),
      **(if ex
           { exception: ex, backtrace: ex.backtrace.take_while do |line|
             !line.include?('lazy_graph/node.rb')
           end }
         else
           {}
         end
        )
    )
  end
  result
end

#fetch_item(input, key, stack) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/lazy_graph/node.rb', line 203

def fetch_item(input, key, stack)
  return MissingValue { key } unless input

  has_value = \
    case input
    when Array then input.length > key && input[key]
    when Hash, Struct then input.key?(key) && !input[key].is_a?(MissingValue)
    end

  if has_value
    value = input[key]
    value = cast(value) if value || @is_boolean
    return input[key] = value
  end

  return input[key] = @default unless derived

  if @copy_input
    copy_item!(input, key, stack, @inputs.first)
  else
    derive_item!(input, key, stack)
  end
end

#find_resolver_for(segment) ⇒ Object



199
200
201
# File 'lib/lazy_graph/node.rb', line 199

def find_resolver_for(segment)
  segment == :'$' ? root : @parent&.find_resolver_for(segment)
end

#lazy_init_node!(input, key) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/lazy_graph/node.rb', line 131

def lazy_init_node!(input, key)
  case input
  when Hash
    node = Node.new(key, "#{path}.#{key}", { type: :object }, self)
    node.children = { properties: {}, pattern_properties: [] }
    node
  when Array
    node = Node.new(key, :"#{path}.#{key}[]", { type: :array }, self)
    child_type = \
      case input.first
      when Hash then :object
      when Array then :array
      end
    node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
    node.children.children = { properties: {}, pattern_properties: [] } if child_type == :object
    node
  else
    Node.new(key, :"#{path}.#{key}", {}, self)
  end
end

#resolve(path, stack_memory, should_recycle = stack_memory) ⇒ Object

When we assign children to a node, we preemptively extract the properties, and pattern properties in both hash and array form. This micro-optimization pays off when we resolve values in the graph at very high frequency.



120
121
122
123
124
125
126
127
128
129
# File 'lib/lazy_graph/node.rb', line 120

def resolve(
  path,
  stack_memory,
  should_recycle = stack_memory,
  **
)
  path.empty? ? stack_memory.frame : MissingValue()
ensure
  should_recycle&.recycle!
end

#resolve_input(stack_memory, path, key) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/lazy_graph/node.rb', line 164

def resolve_input(stack_memory, path, key)
  input_id = key.object_id >> 2 ^ stack_memory.shifted_id
  if @resolution_stack.include?(input_id)
    if @debug
      stack_memory.log_debug(
        property: "#{stack_memory}.#{key}",
        exception: 'Infinite Recursion Detected during dependency resolution'
      )
    end
    return MissingValue { "Infinite Recursion in #{stack_memory} => #{path.to_path_str}" }
  end

  @resolution_stack << (input_id)
  first_segment = path.segment.part

  resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : @parent).find_resolver_for(first_segment)

  if resolver_node
    input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
    resolver_node.resolve(
      first_segment == :'$' ? path.next : path,
      input_frame_pointer,
      nil
    )
  else
    MissingValue { path.to_path_str }
  end
ensure
  @resolution_stack.pop
end