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, get_file_body, #interpret_derived_proc, #map_derived_inputs_to_paths, #parse_args_with_conditions, #parse_derived_inputs, #parse_rule_string, #resolver_for, #rule_definition_backtrace

Constructor Details

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

Returns a new instance of Node.


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/lazy_graph/node.rb', line 56

def initialize(name, path, node, parent, debug: false, helpers: nil, namespace: nil)
  @name = name
  @path = path
  @parent = parent
  @debug = debug
  @depth = parent ? parent.depth + 1 : 0
  @root = parent ? parent.root : self
  @rule = node[:rule]
  @rule_location = node[:rule_location]
  @type = node[:type]
  @validate_presence = node[:validate_presence]
  @helpers = helpers
  @invisible = debug.eql?(true) ? false : node[:invisible]
  @visited = {}.compare_by_identity
  @namespace = namespace

  instance_variable_set("@is_#{@type}", true)
  define_singleton_method(:cast, build_caster)
  define_singleton_method(:trace!, proc { |*| }) unless @debug

  define_missing_value_proc!

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

  # Simple nodes are not a container type, and do not have rule or default
  @simple = !(%i[object array date time timestamp decimal].include?(@type) || node[:rule] || @has_default)
end

Instance Attribute Details

#childrenObject

Returns the value of attribute children.


51
52
53
# File 'lib/lazy_graph/node.rb', line 51

def children
  @children
end

#depthObject

Returns the value of attribute depth.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def depth
  @depth
end

#derivedObject

Returns the value of attribute derived.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def derived
  @derived
end

#invisibleObject

Returns the value of attribute invisible.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def invisible
  @invisible
end

#is_objectObject (readonly)

Returns the value of attribute is_object.


52
53
54
# File 'lib/lazy_graph/node.rb', line 52

def is_object
  @is_object
end

#nameObject

Returns the value of attribute name.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def name
  @name
end

#namespaceObject (readonly)

Returns the value of attribute namespace.


52
53
54
# File 'lib/lazy_graph/node.rb', line 52

def namespace
  @namespace
end

#parentObject

Returns the value of attribute parent.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def parent
  @parent
end

#pathObject

Returns the value of attribute path.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def path
  @path
end

#rootObject

Returns the value of attribute root.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def root
  @root
end

#typeObject

Returns the value of attribute type.


50
51
52
# File 'lib/lazy_graph/node.rb', line 50

def type
  @type
end

Instance Method Details

#absolute_pathObject


201
202
203
204
205
206
207
208
209
210
211
# File 'lib/lazy_graph/node.rb', line 201

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


213
214
215
# File 'lib/lazy_graph/node.rb', line 213

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

#build_casterObject


117
118
119
120
121
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
148
149
150
151
152
# File 'lib/lazy_graph/node.rb', line 117

def build_caster
  if @is_decimal
    ->(value) { value.is_a?(BigDecimal) ? value : value.to_d }
  elsif @is_date
    lambda { |value|
      if value.is_a?(String)
        Date.parse(value)
      else
        value.is_a?(Symbol) ? Date.parse(value.to_s) : value
      end
    }
  elsif @is_boolean
    lambda do |value|
      if value.is_a?(TrueClass) || value.is_a?(FalseClass)
        value
      else
        value.is_a?(MissingValue) ? false : !!value
      end
    end
  elsif @is_timestamp
    lambda do |value|
      case value
      when String
        DateTime.parse(value).to_time
      when Numeric
        Time.at(value)
      else
        value
      end
    end
  elsif @is_string
    lambda(&:to_s)
  else
    ->(value) { value }
  end
end

#build_derived_inputs!Object


85
86
87
88
89
90
91
92
93
94
# File 'lib/lazy_graph/node.rb', line 85

def build_derived_inputs!
  build_derived_inputs(@rule, @helpers) if @rule
  return unless @children
  return @children.build_derived_inputs! if @children.is_a?(Node)

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

#clear_visits!Object


154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/lazy_graph/node.rb', line 154

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, resolver, _i, segments) ⇒ Object


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
# File 'lib/lazy_graph/node.rb', line 267

def copy_item!(input, key, stack, (path, resolver, _i, segments))
  missing_value = resolver ? nil : MissingValue { key }
  if resolver && segments
    parts = path.parts.dup
    parts_identity = path.identity
    segments.each do |index, resolver|
      break missing_value = MissingValue { key } unless resolver

      part = resolver.resolve_relative_input(stack, parts[index].options.first)
      if part.is_a?(MissingValue)
        raise_presence_validation_error!(stack, key, parts[index].options.first) if @validate_presence
        break missing_value = part
      end

      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(resolver.resolve_relative_input(stack, path))

  if result.nil? || result.is_a?(MissingValue)
    raise_presence_validation_error!(stack, key, path) if @validate_presence
    input[key] = MissingValue { key }
  else
    input[key] = result
  end
end

#define_missing_value_proc!Object


96
97
98
99
100
101
# File 'lib/lazy_graph/node.rb', line 96

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


298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/lazy_graph/node.rb', line 298

def derive_item!(input, key, stack)
  @inputs.each do |path, resolver, i, segments|
    if segments
      missing_value = nil
      parts = path.parts.dup
      parts_identity = path.identity
      segments.each do |index, resolver|
        break missing_value = MissingValue { key } unless resolver

        part = resolver.resolve_relative_input(stack, parts[index].options.first)
        if part.is_a?(MissingValue)
          raise_presence_validation_error!(stack, key, parts[index].options.first) if @validate_presence
          break missing_value = part
        end

        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 = begin
      missing_value || resolver.resolve_relative_input(stack, path)
    rescue AbortError, ValidationError => e
      raise e
    rescue StandardError => e
      ex = e
      LazyGraph.logger.error("Error in #{self.path}")
      LazyGraph.logger.error(e)
      LazyGraph.logger.error(e.backtrace.take_while do |line|
        !line.include?('lazy_graph/node.rb')
      end.join("\n"))

      MissingValue { "#{key} raised exception: #{e.message}" }
    end

    if result.nil? || result.is_a?(MissingValue)
      raise_presence_validation_error!(stack, key, path) if @validate_presence

      @node_context[i] = nil
    else
      @node_context[i] = result
    end
  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 AbortError, ValidationError => e
        raise e
      rescue StandardError => e
        ex = e
        LazyGraph.logger.error(e)
        LazyGraph.logger.error(e.backtrace.take_while do |line|
          !line.include?('lazy_graph/node.rb')
        end.join("\n"))

        if ENV['LAZYGRAPH_OPEN_ON_ERROR'] && !@revealed_src
          require 'shellwords'
          @revealed_src = true
          `sh -c \"$EDITOR '#{Shellwords.escape(e.backtrace.first[/.*:/][...-1])}'\" `
        end

        MissingValue { "#{key} raised exception: #{e.message}" }
      end

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

  if conditions_passed
    trace!(stack, exception: ex) do
      {
        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 } : {})
      }
    end
  end

  result
end

#fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil) ⇒ Object


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

def fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil)
  item = fetch_item(input, segment, stack_memory)
  unless @simple || item.is_a?(MissingValue)
    item = resolve(
      path,
      stack_memory.push(item, segment)
    )
  end

  item = cast(item) if @simple

  preserve_keys ? preserve_keys[segment] = item : item
end

#fetch_item(input, key, stack) ⇒ Object


234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/lazy_graph/node.rb', line 234

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 stack.recursion_depth >= 8
    input_id = key.object_id >> 2 ^ input.object_id << 28
    if @resolution_stack.key?(input_id)
      trace!(stack, exception: 'Infinite Recursion Detected during dependency resolution') do
        { output: :"#{stack}.#{key}" }
      end
      return MissingValue { "Infinite Recursion in #{stack} => #{key}" }
    end
    @resolution_stack[input_id] = true
  end

  @copy_input ? copy_item!(input, key, stack, @inputs.first) : derive_item!(input, key, stack)
ensure
  @resolution_stack.delete(input_id) if input_id
end

#find_resolver_for(segment) ⇒ Object


217
218
219
# File 'lib/lazy_graph/node.rb', line 217

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

#lazy_init_node!(input, key) ⇒ Object


180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/lazy_graph/node.rb', line 180

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.equal? :object
    node
  else
    Node.new(key, :"#{path}.#{key}", {}, self)
  end
end

#raise_presence_validation_error!(stack, key, path) ⇒ Object

Raises:


406
407
408
409
# File 'lib/lazy_graph/node.rb', line 406

def raise_presence_validation_error!(stack, key, path)
  raise ValidationError,
        "Missing required value for #{stack}.#{key} at #{path.to_path_str}"
end

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


169
170
171
172
173
174
175
176
177
178
# File 'lib/lazy_graph/node.rb', line 169

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

#resolve_relative_input(stack_memory, path) ⇒ Object


221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/lazy_graph/node.rb', line 221

def resolve_relative_input(stack_memory, path)
  input_frame_pointer = path.absolute? ? stack_memory.root : stack_memory.ptr_at(depth - 1)
  input_frame_pointer.recursion_depth += 1

  return cast(input_frame_pointer.frame[path.first_path_segment.part]) if @simple

  fetch_and_resolve(
    path.absolute? ? path.next.next : path.next, input_frame_pointer.frame, path.first_path_segment.part, input_frame_pointer
  )
ensure
  input_frame_pointer.recursion_depth -= 1
end

#simple?Boolean

Returns:

  • (Boolean)

54
# File 'lib/lazy_graph/node.rb', line 54

def simple? = @simple

#trace!(stack, exception: nil) ⇒ Object


393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/lazy_graph/node.rb', line 393

def trace!(stack, exception: nil)
  return if @debug == 'exceptions' && !exception

  trace_opts = {
    **yield,
    **(exception ? { exception: exception } : {})
  }

  return if @debug.is_a?(Regexp) && !(@debug =~ trace_opts[:output])

  stack.log_debug(**trace_opts)
end