lazy_graph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.rubocop.yml +81 -0
 - data/CODE_OF_CONDUCT.md +84 -0
 - data/LICENSE.txt +21 -0
 - data/README.md +562 -0
 - data/Rakefile +4 -0
 - data/examples/performance_tests.rb +117 -0
 - data/lib/lazy_graph/builder/dsl.rb +315 -0
 - data/lib/lazy_graph/builder.rb +138 -0
 - data/lib/lazy_graph/builder_group.rb +57 -0
 - data/lib/lazy_graph/context.rb +60 -0
 - data/lib/lazy_graph/graph.rb +73 -0
 - data/lib/lazy_graph/hash_utils.rb +87 -0
 - data/lib/lazy_graph/lazy-graph.json +148 -0
 - data/lib/lazy_graph/missing_value.rb +26 -0
 - data/lib/lazy_graph/node/array_node.rb +67 -0
 - data/lib/lazy_graph/node/derived_rules.rb +196 -0
 - data/lib/lazy_graph/node/node_properties.rb +64 -0
 - data/lib/lazy_graph/node/object_node.rb +113 -0
 - data/lib/lazy_graph/node.rb +316 -0
 - data/lib/lazy_graph/path_parser/path.rb +46 -0
 - data/lib/lazy_graph/path_parser/path_group.rb +12 -0
 - data/lib/lazy_graph/path_parser/path_part.rb +13 -0
 - data/lib/lazy_graph/path_parser.rb +211 -0
 - data/lib/lazy_graph/server.rb +86 -0
 - data/lib/lazy_graph/stack_pointer.rb +56 -0
 - data/lib/lazy_graph/version.rb +5 -0
 - data/lib/lazy_graph.rb +32 -0
 - data/logo.png +0 -0
 - metadata +200 -0
 
| 
         @@ -0,0 +1,113 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module LazyGraph
         
     | 
| 
      
 2 
     | 
    
         
            +
              class ObjectNode < Node
         
     | 
| 
      
 3 
     | 
    
         
            +
                # An object supports the following types of path resolutions.
         
     | 
| 
      
 4 
     | 
    
         
            +
                # 1. Property name: obj.property => value
         
     | 
| 
      
 5 
     | 
    
         
            +
                # 2. Property name group: obj[property1, property2] =>  { property1: value1, property2: value2 }
         
     | 
| 
      
 6 
     | 
    
         
            +
                # 3. All [*]
         
     | 
| 
      
 7 
     | 
    
         
            +
                def resolve(
         
     | 
| 
      
 8 
     | 
    
         
            +
                  path,
         
     | 
| 
      
 9 
     | 
    
         
            +
                  stack_memory,
         
     | 
| 
      
 10 
     | 
    
         
            +
                  should_recycle = stack_memory,
         
     | 
| 
      
 11 
     | 
    
         
            +
                  preserve_keys: false
         
     | 
| 
      
 12 
     | 
    
         
            +
                )
         
     | 
| 
      
 13 
     | 
    
         
            +
                  input = stack_memory.frame
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @visited[input.object_id ^ (path.object_id << 8)] ||= begin
         
     | 
| 
      
 15 
     | 
    
         
            +
                    if (path_segment = path.segment).is_a?(PathParser::PathGroup)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
         
     | 
| 
      
 17 
     | 
    
         
            +
                        resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      end
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    if !segment = path_segment&.part
         
     | 
| 
      
 22 
     | 
    
         
            +
                      @properties_a.each do |key, node|
         
     | 
| 
      
 23 
     | 
    
         
            +
                        item = node.fetch_item(input, key, stack_memory)
         
     | 
| 
      
 24 
     | 
    
         
            +
                        node.resolve(path.next, stack_memory.push(item, key))
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
                      if @pattern_properties_a.any? && input.keys.length > @properties_a.length
         
     | 
| 
      
 27 
     | 
    
         
            +
                        input.each_key do |key|
         
     | 
| 
      
 28 
     | 
    
         
            +
                          node = !@properties[key] && @pattern_properties_a.find { |pattern, _value| pattern.match?(key) }&.last
         
     | 
| 
      
 29 
     | 
    
         
            +
                          item = node.fetch_item(input, key, stack_memory)
         
     | 
| 
      
 30 
     | 
    
         
            +
                          node.resolve(path.next, stack_memory.push(item, key))
         
     | 
| 
      
 31 
     | 
    
         
            +
                        end
         
     | 
| 
      
 32 
     | 
    
         
            +
                      end
         
     | 
| 
      
 33 
     | 
    
         
            +
                      input
         
     | 
| 
      
 34 
     | 
    
         
            +
                    elsif (prop = @properties[segment])
         
     | 
| 
      
 35 
     | 
    
         
            +
                      item = prop.fetch_item(input, segment, stack_memory)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      value = prop.resolve(
         
     | 
| 
      
 37 
     | 
    
         
            +
                        path.next, stack_memory.push(item, segment)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      )
         
     | 
| 
      
 39 
     | 
    
         
            +
                      preserve_keys ? preserve_keys[segment] = value : value
         
     | 
| 
      
 40 
     | 
    
         
            +
                    elsif segment == :*
         
     | 
| 
      
 41 
     | 
    
         
            +
                      # rubocop:disable
         
     | 
| 
      
 42 
     | 
    
         
            +
                      (input.keys | @properties_a.map(&:first)).each do |key|
         
     | 
| 
      
 43 
     | 
    
         
            +
                        next unless (node = @properties[key] || @pattern_properties_a.find do |pattern, _value|
         
     | 
| 
      
 44 
     | 
    
         
            +
                          pattern.match?(key)
         
     | 
| 
      
 45 
     | 
    
         
            +
                        end&.last)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                        item = node.fetch_item(input, key, stack_memory)
         
     | 
| 
      
 48 
     | 
    
         
            +
                        preserve_keys[key] = node.resolve(path.next, stack_memory.push(item, key))
         
     | 
| 
      
 49 
     | 
    
         
            +
                      end
         
     | 
| 
      
 50 
     | 
    
         
            +
                    elsif (_, prop = @pattern_properties_a.find { |key, _val| key.match?(segment) })
         
     | 
| 
      
 51 
     | 
    
         
            +
                      item = prop.fetch_item(input, segment, stack_memory)
         
     | 
| 
      
 52 
     | 
    
         
            +
                      value = prop.resolve(
         
     | 
| 
      
 53 
     | 
    
         
            +
                        path.next, stack_memory.push(item, segment)
         
     | 
| 
      
 54 
     | 
    
         
            +
                      )
         
     | 
| 
      
 55 
     | 
    
         
            +
                      preserve_keys ? preserve_keys[segment] = value : value
         
     | 
| 
      
 56 
     | 
    
         
            +
                    elsif input.key?(segment)
         
     | 
| 
      
 57 
     | 
    
         
            +
                      prop = @properties[segment] = lazy_init_node!(input[segment], segment)
         
     | 
| 
      
 58 
     | 
    
         
            +
                      @properties_a = @properties.to_a
         
     | 
| 
      
 59 
     | 
    
         
            +
                      item = prop.fetch_item(input, segment, stack_memory)
         
     | 
| 
      
 60 
     | 
    
         
            +
                      value = prop.resolve(
         
     | 
| 
      
 61 
     | 
    
         
            +
                        path.next, stack_memory.push(item, segment)
         
     | 
| 
      
 62 
     | 
    
         
            +
                      )
         
     | 
| 
      
 63 
     | 
    
         
            +
                      preserve_keys ? preserve_keys[segment] = value : value
         
     | 
| 
      
 64 
     | 
    
         
            +
                    else
         
     | 
| 
      
 65 
     | 
    
         
            +
                      value = MissingValue()
         
     | 
| 
      
 66 
     | 
    
         
            +
                      preserve_keys ? preserve_keys[segment] = value : value
         
     | 
| 
      
 67 
     | 
    
         
            +
                    end
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 70 
     | 
    
         
            +
                  should_recycle&.recycle!
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                def find_resolver_for(segment)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  if segment == :'$'
         
     | 
| 
      
 75 
     | 
    
         
            +
                    root
         
     | 
| 
      
 76 
     | 
    
         
            +
                  elsif @properties.key?(segment)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    self
         
     | 
| 
      
 78 
     | 
    
         
            +
                  else
         
     | 
| 
      
 79 
     | 
    
         
            +
                    @parent&.find_resolver_for(segment)
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
                end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def children=(value)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  @children = value
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  @properties = @children.fetch(:properties, {})
         
     | 
| 
      
 87 
     | 
    
         
            +
                  @properties.compare_by_identity
         
     | 
| 
      
 88 
     | 
    
         
            +
                  @pattern_properties = @children.fetch(:pattern_properties, {})
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                  @properties_a = @properties.to_a
         
     | 
| 
      
 91 
     | 
    
         
            +
                  @pattern_properties_a = @pattern_properties.to_a
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  @has_properties = @properties.any? || @pattern_properties.any?
         
     | 
| 
      
 94 
     | 
    
         
            +
                  return if @pattern_properties.any?
         
     | 
| 
      
 95 
     | 
    
         
            +
                  return unless @properties.any?
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                  invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
         
     | 
| 
      
 98 
     | 
    
         
            +
                  @property_class = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
         
     | 
| 
      
 99 
     | 
    
         
            +
                                                       invisible: invisible }]
         
     | 
| 
      
 100 
     | 
    
         
            +
                end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                def cast(value)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  if !@property_class && value.is_a?(Hash)
         
     | 
| 
      
 104 
     | 
    
         
            +
                    value.default_proc = ->(h, k) { k.is_a?(Symbol) ? nil : h[k.to_s.to_sym] }
         
     | 
| 
      
 105 
     | 
    
         
            +
                    value.compare_by_identity
         
     | 
| 
      
 106 
     | 
    
         
            +
                  elsif @property_class && !value.is_a?(@property_class)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    @property_class.new(value.to_h)
         
     | 
| 
      
 108 
     | 
    
         
            +
                  else
         
     | 
| 
      
 109 
     | 
    
         
            +
                    value
         
     | 
| 
      
 110 
     | 
    
         
            +
                  end
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,316 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bigdecimal/util'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module LazyGraph
         
     | 
| 
      
 7 
     | 
    
         
            +
              require_relative 'node/derived_rules'
         
     | 
| 
      
 8 
     | 
    
         
            +
              require_relative 'node/array_node'
         
     | 
| 
      
 9 
     | 
    
         
            +
              require_relative 'node/object_node'
         
     | 
| 
      
 10 
     | 
    
         
            +
              require_relative 'node/node_properties'
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              DIGIT_REGEXP = /^-?\d+$/
         
     | 
| 
      
 13 
     | 
    
         
            +
              SAFE_TOKEN_REGEXP = /^[A-Za-z][A-Za-z0-9]*$/
         
     | 
| 
      
 14 
     | 
    
         
            +
              PROPERTY_CLASSES = Hash.new do |h, members|
         
     | 
| 
      
 15 
     | 
    
         
            +
                h[members] = LazyGraph.const_set("NodeProperties#{h.hash.abs}", NodeProperties.build(**members))
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # Class: Node
         
     | 
| 
      
 19 
     | 
    
         
            +
              # Represents A single Node within our LazyGraph structure
         
     | 
| 
      
 20 
     | 
    
         
            +
              # A node is a logical position with a graph structure.
         
     | 
| 
      
 21 
     | 
    
         
            +
              # The node might capture knowledge about how to derived values at its position
         
     | 
| 
      
 22 
     | 
    
         
            +
              # if a value is not provided.
         
     | 
| 
      
 23 
     | 
    
         
            +
              # This can be in the form of a default value or a derivation rule.
         
     | 
| 
      
 24 
     | 
    
         
            +
              #
         
     | 
| 
      
 25 
     | 
    
         
            +
              # This class is heavily optimized to resolve values in a graph structure
         
     | 
| 
      
 26 
     | 
    
         
            +
              # with as little overhead as possible. (Note heavy use of ivars,
         
     | 
| 
      
 27 
     | 
    
         
            +
              # and minimal method calls in the recursive resolve method).
         
     | 
| 
      
 28 
     | 
    
         
            +
              #
         
     | 
| 
      
 29 
     | 
    
         
            +
              # Nodes support (non-circular) recursive resolution of values, i.e.
         
     | 
| 
      
 30 
     | 
    
         
            +
              # if a node depends on the output of several other nodes in the graph,
         
     | 
| 
      
 31 
     | 
    
         
            +
              # it will resolve those nodes first before resolving itself.
         
     | 
| 
      
 32 
     | 
    
         
            +
              #
         
     | 
| 
      
 33 
     | 
    
         
            +
              # Node resolution maintains a full stack, so that values can be resolved relative to the position
         
     | 
| 
      
 34 
     | 
    
         
            +
              # of the node itself.
         
     | 
| 
      
 35 
     | 
    
         
            +
              #
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              class Node
         
     | 
| 
      
 38 
     | 
    
         
            +
                include DerivedRules
         
     | 
| 
      
 39 
     | 
    
         
            +
                attr_accessor :name, :path, :type, :derived, :depth, :parent, :root, :invisible
         
     | 
| 
      
 40 
     | 
    
         
            +
                attr_accessor :children
         
     | 
| 
      
 41 
     | 
    
         
            +
                attr_reader :is_object
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def initialize(name, path, node, parent, debug: false, helpers: nil)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  @name = name
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @path = path
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @parent = parent
         
     | 
| 
      
 47 
     | 
    
         
            +
                  @debug = debug
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @depth = parent ? parent.depth + 1 : 0
         
     | 
| 
      
 49 
     | 
    
         
            +
                  @root = parent ? parent.root : self
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @type = node[:type]
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @invisible = node[:invisible]
         
     | 
| 
      
 52 
     | 
    
         
            +
                  @visited = {}.compare_by_identity
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  define_missing_value_proc!
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  @has_default = node.key?(:default)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  @default = @has_default ? node[:default] : MissingValue { @name }
         
     | 
| 
      
 58 
     | 
    
         
            +
                  @resolution_stack = []
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  instance_variable_set("@is_#{@type}", true)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  build_derived_inputs(node[:rule], helpers) if node[:rule]
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                def define_missing_value_proc!
         
     | 
| 
      
 65 
     | 
    
         
            +
                  define_singleton_method(
         
     | 
| 
      
 66 
     | 
    
         
            +
                    :MissingValue,
         
     | 
| 
      
 67 
     | 
    
         
            +
                    @debug ? ->(&blk) { MissingValue.new(blk&.call || absolute_path) } : -> { MissingValue::BLANK }
         
     | 
| 
      
 68 
     | 
    
         
            +
                  )
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                def clear_visits!
         
     | 
| 
      
 72 
     | 
    
         
            +
                  @visited.clear
         
     | 
| 
      
 73 
     | 
    
         
            +
                  return unless @children
         
     | 
| 
      
 74 
     | 
    
         
            +
                  return @children.clear_visits! if @children.is_a?(Node)
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  @children[:properties]&.each do |_, node|
         
     | 
| 
      
 77 
     | 
    
         
            +
                    node.clear_visits!
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  @children[:pattern_properties]&.each do |_, node|
         
     | 
| 
      
 81 
     | 
    
         
            +
                    node.clear_visits!
         
     | 
| 
      
 82 
     | 
    
         
            +
                  end
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                # When we assign children to a node, we preemptively extract the properties, and pattern properties
         
     | 
| 
      
 86 
     | 
    
         
            +
                # in both hash and array form. This micro-optimization pays off when we resolve values in the graph at
         
     | 
| 
      
 87 
     | 
    
         
            +
                # very high frequency.
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                def resolve(
         
     | 
| 
      
 90 
     | 
    
         
            +
                  path,
         
     | 
| 
      
 91 
     | 
    
         
            +
                  stack_memory,
         
     | 
| 
      
 92 
     | 
    
         
            +
                  should_recycle = stack_memory,
         
     | 
| 
      
 93 
     | 
    
         
            +
                  **
         
     | 
| 
      
 94 
     | 
    
         
            +
                )
         
     | 
| 
      
 95 
     | 
    
         
            +
                  path.empty? ? stack_memory.frame : MissingValue()
         
     | 
| 
      
 96 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 97 
     | 
    
         
            +
                  should_recycle&.recycle!
         
     | 
| 
      
 98 
     | 
    
         
            +
                end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                def lazy_init_node!(input, key)
         
     | 
| 
      
 101 
     | 
    
         
            +
                  case input
         
     | 
| 
      
 102 
     | 
    
         
            +
                  when Hash
         
     | 
| 
      
 103 
     | 
    
         
            +
                    node = Node.new(key, "#{path}.#{key}", { type: :object }, self)
         
     | 
| 
      
 104 
     | 
    
         
            +
                    node.children = { properties: {}, pattern_properties: {} }
         
     | 
| 
      
 105 
     | 
    
         
            +
                    node
         
     | 
| 
      
 106 
     | 
    
         
            +
                  when Array
         
     | 
| 
      
 107 
     | 
    
         
            +
                    node = Node.new(key, :"#{path}.#{key}[]", { type: :array }, self)
         
     | 
| 
      
 108 
     | 
    
         
            +
                    child_type = \
         
     | 
| 
      
 109 
     | 
    
         
            +
                      case input.first
         
     | 
| 
      
 110 
     | 
    
         
            +
                      when Hash then :object
         
     | 
| 
      
 111 
     | 
    
         
            +
                      when Array then :array
         
     | 
| 
      
 112 
     | 
    
         
            +
                      end
         
     | 
| 
      
 113 
     | 
    
         
            +
                    node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    node.children.children = { properties: {}, pattern_properties: {} } if child_type == :object
         
     | 
| 
      
 115 
     | 
    
         
            +
                    node
         
     | 
| 
      
 116 
     | 
    
         
            +
                  else
         
     | 
| 
      
 117 
     | 
    
         
            +
                    Node.new(key, :"#{path}.#{key}", {}, self)
         
     | 
| 
      
 118 
     | 
    
         
            +
                  end
         
     | 
| 
      
 119 
     | 
    
         
            +
                end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                def absolute_path
         
     | 
| 
      
 122 
     | 
    
         
            +
                  @absolute_path ||= begin
         
     | 
| 
      
 123 
     | 
    
         
            +
                    next_node = self
         
     | 
| 
      
 124 
     | 
    
         
            +
                    path = []
         
     | 
| 
      
 125 
     | 
    
         
            +
                    while next_node
         
     | 
| 
      
 126 
     | 
    
         
            +
                      path << next_node.name
         
     | 
| 
      
 127 
     | 
    
         
            +
                      next_node = next_node.parent
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
                    path.reverse.join('.')
         
     | 
| 
      
 130 
     | 
    
         
            +
                  end
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                def resolve_input(stack_memory, path, key)
         
     | 
| 
      
 134 
     | 
    
         
            +
                  input_id = key.object_id ^ (stack_memory.object_id << 8)
         
     | 
| 
      
 135 
     | 
    
         
            +
                  if @resolution_stack.include?(input_id)
         
     | 
| 
      
 136 
     | 
    
         
            +
                    if @debug
         
     | 
| 
      
 137 
     | 
    
         
            +
                      stack_memory.log_debug(
         
     | 
| 
      
 138 
     | 
    
         
            +
                        property: "#{stack_memory}.#{key}",
         
     | 
| 
      
 139 
     | 
    
         
            +
                        exception: 'Infinite Recursion Detected during dependency resolution'
         
     | 
| 
      
 140 
     | 
    
         
            +
                      )
         
     | 
| 
      
 141 
     | 
    
         
            +
                    end
         
     | 
| 
      
 142 
     | 
    
         
            +
                    return MissingValue { "Infinite Recursion in #{stack_memory} => #{path.to_path_str}" }
         
     | 
| 
      
 143 
     | 
    
         
            +
                  end
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                  @resolution_stack << (input_id)
         
     | 
| 
      
 146 
     | 
    
         
            +
                  first_segment = path.parts.first.part
         
     | 
| 
      
 147 
     | 
    
         
            +
                  resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : parent).find_resolver_for(first_segment)
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                  if resolver_node
         
     | 
| 
      
 150 
     | 
    
         
            +
                    input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
         
     | 
| 
      
 151 
     | 
    
         
            +
                    resolver_node.resolve(
         
     | 
| 
      
 152 
     | 
    
         
            +
                      first_segment == :'$' ? path.next : path,
         
     | 
| 
      
 153 
     | 
    
         
            +
                      input_frame_pointer,
         
     | 
| 
      
 154 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 155 
     | 
    
         
            +
                    )
         
     | 
| 
      
 156 
     | 
    
         
            +
                  else
         
     | 
| 
      
 157 
     | 
    
         
            +
                    MissingValue { path.to_path_str }
         
     | 
| 
      
 158 
     | 
    
         
            +
                  end
         
     | 
| 
      
 159 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 160 
     | 
    
         
            +
                  @resolution_stack.pop
         
     | 
| 
      
 161 
     | 
    
         
            +
                end
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                def ancestors
         
     | 
| 
      
 164 
     | 
    
         
            +
                  @ancestors ||= [self, *(parent ? parent.ancestors : [])]
         
     | 
| 
      
 165 
     | 
    
         
            +
                end
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                def find_resolver_for(segment)
         
     | 
| 
      
 168 
     | 
    
         
            +
                  segment == :'$' ? root : @parent&.find_resolver_for(segment)
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                def cast(value)
         
     | 
| 
      
 172 
     | 
    
         
            +
                  if @is_decimal
         
     | 
| 
      
 173 
     | 
    
         
            +
                    value.is_a?(BigDecimal) ? value : value.to_d
         
     | 
| 
      
 174 
     | 
    
         
            +
                  elsif @is_date
         
     | 
| 
      
 175 
     | 
    
         
            +
                    value.is_a?(String) ? Date.parse(value) : value
         
     | 
| 
      
 176 
     | 
    
         
            +
                  elsif @is_boolean
         
     | 
| 
      
 177 
     | 
    
         
            +
                    if value.is_a?(TrueClass) || value.is_a?(FalseClass)
         
     | 
| 
      
 178 
     | 
    
         
            +
                      value
         
     | 
| 
      
 179 
     | 
    
         
            +
                    else
         
     | 
| 
      
 180 
     | 
    
         
            +
                      value.is_a?(MissingValue) ? false : !!value
         
     | 
| 
      
 181 
     | 
    
         
            +
                    end
         
     | 
| 
      
 182 
     | 
    
         
            +
                  elsif @is_timestamp
         
     | 
| 
      
 183 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 184 
     | 
    
         
            +
                    when String then DateTime.parse(value).to_time
         
     | 
| 
      
 185 
     | 
    
         
            +
                    when Numeric then Time.at(value)
         
     | 
| 
      
 186 
     | 
    
         
            +
                    else value
         
     | 
| 
      
 187 
     | 
    
         
            +
                    end
         
     | 
| 
      
 188 
     | 
    
         
            +
                  else
         
     | 
| 
      
 189 
     | 
    
         
            +
                    value
         
     | 
| 
      
 190 
     | 
    
         
            +
                  end
         
     | 
| 
      
 191 
     | 
    
         
            +
                end
         
     | 
| 
      
 192 
     | 
    
         
            +
             
     | 
| 
      
 193 
     | 
    
         
            +
                def fetch_item(input, key, stack)
         
     | 
| 
      
 194 
     | 
    
         
            +
                  return MissingValue { key } unless input
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                  has_value = \
         
     | 
| 
      
 197 
     | 
    
         
            +
                    case input
         
     | 
| 
      
 198 
     | 
    
         
            +
                    when Array then input.length > key && input[key]
         
     | 
| 
      
 199 
     | 
    
         
            +
                    when Hash, Struct then input.key?(key) && !input[key].is_a?(MissingValue)
         
     | 
| 
      
 200 
     | 
    
         
            +
                    end
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
      
 202 
     | 
    
         
            +
                  if has_value
         
     | 
| 
      
 203 
     | 
    
         
            +
                    value = input[key]
         
     | 
| 
      
 204 
     | 
    
         
            +
                    value = cast(value) if value || @is_boolean
         
     | 
| 
      
 205 
     | 
    
         
            +
                    return input[key] = value
         
     | 
| 
      
 206 
     | 
    
         
            +
                  end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                  return input[key] = @default unless derived
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                  if @copy_input
         
     | 
| 
      
 211 
     | 
    
         
            +
                    copy_item!(input, key, stack, @inputs.first)
         
     | 
| 
      
 212 
     | 
    
         
            +
                  else
         
     | 
| 
      
 213 
     | 
    
         
            +
                    derive_item!(input, key, stack)
         
     | 
| 
      
 214 
     | 
    
         
            +
                  end
         
     | 
| 
      
 215 
     | 
    
         
            +
                end
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
      
 217 
     | 
    
         
            +
                def copy_item!(input, key, stack, (path, i, segment_indexes))
         
     | 
| 
      
 218 
     | 
    
         
            +
                  if segment_indexes
         
     | 
| 
      
 219 
     | 
    
         
            +
                    missing_value = nil
         
     | 
| 
      
 220 
     | 
    
         
            +
                    parts = path.parts.dup
         
     | 
| 
      
 221 
     | 
    
         
            +
                    parts_identity = path.identity
         
     | 
| 
      
 222 
     | 
    
         
            +
                    segment_indexes.each do |index|
         
     | 
| 
      
 223 
     | 
    
         
            +
                      part = resolve_input(stack, parts[index].options.first, key)
         
     | 
| 
      
 224 
     | 
    
         
            +
                      break missing_value = part if part.is_a?(MissingValue)
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                      part_sym = part.to_s.to_sym
         
     | 
| 
      
 227 
     | 
    
         
            +
                      parts_identity ^= part_sym.object_id << index
         
     | 
| 
      
 228 
     | 
    
         
            +
                      parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
         
     | 
| 
      
 229 
     | 
    
         
            +
                    end
         
     | 
| 
      
 230 
     | 
    
         
            +
                    path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
         
     | 
| 
      
 231 
     | 
    
         
            +
                  end
         
     | 
| 
      
 232 
     | 
    
         
            +
             
     | 
| 
      
 233 
     | 
    
         
            +
                  result = missing_value || cast(resolve_input(stack, path, key))
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
                  if @debug
         
     | 
| 
      
 236 
     | 
    
         
            +
                    stack.log_debug(
         
     | 
| 
      
 237 
     | 
    
         
            +
                      output: :"#{stack}.#{key}",
         
     | 
| 
      
 238 
     | 
    
         
            +
                      result: result,
         
     | 
| 
      
 239 
     | 
    
         
            +
                      inputs: @node_context.to_h.except(:itself, :stack_ptr),
         
     | 
| 
      
 240 
     | 
    
         
            +
                      calc: @src
         
     | 
| 
      
 241 
     | 
    
         
            +
                    )
         
     | 
| 
      
 242 
     | 
    
         
            +
                  end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
                  input[key] = result.nil? ? MissingValue { key } : result
         
     | 
| 
      
 245 
     | 
    
         
            +
                end
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
                def derive_item!(input, key, stack)
         
     | 
| 
      
 248 
     | 
    
         
            +
                  @inputs.each do |path, i, segment_indexes|
         
     | 
| 
      
 249 
     | 
    
         
            +
                    if segment_indexes
         
     | 
| 
      
 250 
     | 
    
         
            +
                      missing_value = nil
         
     | 
| 
      
 251 
     | 
    
         
            +
                      parts = path.parts.dup
         
     | 
| 
      
 252 
     | 
    
         
            +
                      parts_identity = path.identity
         
     | 
| 
      
 253 
     | 
    
         
            +
                      segment_indexes.each do |index|
         
     | 
| 
      
 254 
     | 
    
         
            +
                        part = resolve_input(stack, parts[index].options.first, key)
         
     | 
| 
      
 255 
     | 
    
         
            +
                        break missing_value = part if part.is_a?(MissingValue)
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
                        part_sym = part.to_s.to_sym
         
     | 
| 
      
 258 
     | 
    
         
            +
                        parts_identity ^= part_sym.object_id << index
         
     | 
| 
      
 259 
     | 
    
         
            +
                        parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
         
     | 
| 
      
 260 
     | 
    
         
            +
                      end
         
     | 
| 
      
 261 
     | 
    
         
            +
                      path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
         
     | 
| 
      
 262 
     | 
    
         
            +
                    end
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
                    result = missing_value || resolve_input(stack, path, key)
         
     | 
| 
      
 265 
     | 
    
         
            +
                    @node_context[i] = result.is_a?(MissingValue) ? nil : result
         
     | 
| 
      
 266 
     | 
    
         
            +
                  end
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                  @node_context[:itself] = input
         
     | 
| 
      
 269 
     | 
    
         
            +
                  @node_context[:stack_ptr] = stack
         
     | 
| 
      
 270 
     | 
    
         
            +
             
     | 
| 
      
 271 
     | 
    
         
            +
                  conditions_passed = !@conditions || @conditions.all? do |field, allowed_value|
         
     | 
| 
      
 272 
     | 
    
         
            +
                    allowed_value.is_a?(Array) ? allowed_value.include?(@node_context[field]) : allowed_value == @node_context[field]
         
     | 
| 
      
 273 
     | 
    
         
            +
                  end
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                  ex = nil
         
     | 
| 
      
 276 
     | 
    
         
            +
                  result = \
         
     | 
| 
      
 277 
     | 
    
         
            +
                    if conditions_passed
         
     | 
| 
      
 278 
     | 
    
         
            +
                      output = begin
         
     | 
| 
      
 279 
     | 
    
         
            +
                        cast(@node_context.process!)
         
     | 
| 
      
 280 
     | 
    
         
            +
                      rescue LazyGraph::AbortError => e
         
     | 
| 
      
 281 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 282 
     | 
    
         
            +
                      rescue StandardError => e
         
     | 
| 
      
 283 
     | 
    
         
            +
                        ex = e
         
     | 
| 
      
 284 
     | 
    
         
            +
                        MissingValue { "#{key} raised exception: #{e.message}" }
         
     | 
| 
      
 285 
     | 
    
         
            +
                      end
         
     | 
| 
      
 286 
     | 
    
         
            +
                      output = output.dup if @has_properties
         
     | 
| 
      
 287 
     | 
    
         
            +
                      input[key] = output.nil? ? MissingValue { key } : output
         
     | 
| 
      
 288 
     | 
    
         
            +
                    else
         
     | 
| 
      
 289 
     | 
    
         
            +
                      MissingValue { key }
         
     | 
| 
      
 290 
     | 
    
         
            +
                    end
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
                  if @debug
         
     | 
| 
      
 293 
     | 
    
         
            +
                    stack.log_debug(
         
     | 
| 
      
 294 
     | 
    
         
            +
                      output: :"#{stack}.#{key}",
         
     | 
| 
      
 295 
     | 
    
         
            +
                      result: result,
         
     | 
| 
      
 296 
     | 
    
         
            +
                      inputs: @node_context.to_h.except(:itself, :stack_ptr),
         
     | 
| 
      
 297 
     | 
    
         
            +
                      calc: @src,
         
     | 
| 
      
 298 
     | 
    
         
            +
                      **(@conditions ? { conditions: @conditions } : {}),
         
     | 
| 
      
 299 
     | 
    
         
            +
                      **(
         
     | 
| 
      
 300 
     | 
    
         
            +
                        if ex
         
     | 
| 
      
 301 
     | 
    
         
            +
                          {
         
     | 
| 
      
 302 
     | 
    
         
            +
                            exception: ex,
         
     | 
| 
      
 303 
     | 
    
         
            +
                            backtrace: ex.backtrace.take_while do |line|
         
     | 
| 
      
 304 
     | 
    
         
            +
                              !line.include?('lazy_graph/node.rb')
         
     | 
| 
      
 305 
     | 
    
         
            +
                            end
         
     | 
| 
      
 306 
     | 
    
         
            +
                          }
         
     | 
| 
      
 307 
     | 
    
         
            +
                        else
         
     | 
| 
      
 308 
     | 
    
         
            +
                          {}
         
     | 
| 
      
 309 
     | 
    
         
            +
                        end
         
     | 
| 
      
 310 
     | 
    
         
            +
                      )
         
     | 
| 
      
 311 
     | 
    
         
            +
                    )
         
     | 
| 
      
 312 
     | 
    
         
            +
                  end
         
     | 
| 
      
 313 
     | 
    
         
            +
                  result
         
     | 
| 
      
 314 
     | 
    
         
            +
                end
         
     | 
| 
      
 315 
     | 
    
         
            +
              end
         
     | 
| 
      
 316 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,46 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module LazyGraph
         
     | 
| 
      
 4 
     | 
    
         
            +
              module PathParser
         
     | 
| 
      
 5 
     | 
    
         
            +
                # This module is responsible for parsing complex path strings into structured components.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                # Path represents a structured component of a complex path string.
         
     | 
| 
      
 8 
     | 
    
         
            +
                # It provides methods to navigate and manipulate the path parts.
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                Path = Struct.new(:parts, keyword_init: true) do
         
     | 
| 
      
 11 
     | 
    
         
            +
                  def next        = @next ||= parts.length <= 1 ? Path::BLANK : Path.new(parts: parts[1..])
         
     | 
| 
      
 12 
     | 
    
         
            +
                  def empty?      = @empty ||= parts.empty?
         
     | 
| 
      
 13 
     | 
    
         
            +
                  def segment     = @segment ||= parts&.[](0)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  def index?      = @index   ||= parts.any? && parts.first.index?
         
     | 
| 
      
 15 
     | 
    
         
            +
                  def identity    = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 4) }
         
     | 
| 
      
 16 
     | 
    
         
            +
                  def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def merge(other)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    (@merged ||= {})[other] ||= \
         
     | 
| 
      
 20 
     | 
    
         
            +
                      if other.empty?
         
     | 
| 
      
 21 
     | 
    
         
            +
                        self
         
     | 
| 
      
 22 
     | 
    
         
            +
                      else
         
     | 
| 
      
 23 
     | 
    
         
            +
                        empty? ? other : Path.new(parts: parts + other.parts)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  def to_path_str
         
     | 
| 
      
 28 
     | 
    
         
            +
                    @to_path_str ||= create_path_str
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  private
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def create_path_str
         
     | 
| 
      
 34 
     | 
    
         
            +
                    parts.inject('$') do |path_str, part|
         
     | 
| 
      
 35 
     | 
    
         
            +
                      path_str + \
         
     | 
| 
      
 36 
     | 
    
         
            +
                        if part.is_a?(PathPart)
         
     | 
| 
      
 37 
     | 
    
         
            +
                          ".#{part.part}"
         
     | 
| 
      
 38 
     | 
    
         
            +
                        else
         
     | 
| 
      
 39 
     | 
    
         
            +
                          "[#{part.options.map(&:to_path_str).join(',').delete_prefix('$.')}]"
         
     | 
| 
      
 40 
     | 
    
         
            +
                        end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
                Path::BLANK = Path.new(parts: [])
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,12 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module LazyGraph
         
     | 
| 
      
 4 
     | 
    
         
            +
              module PathParser
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Represents a group of paths with a list of options, which must all be resolved.
         
     | 
| 
      
 6 
     | 
    
         
            +
                PathGroup = Struct.new(:options, keyword_init: true) do
         
     | 
| 
      
 7 
     | 
    
         
            +
                  def index?
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @index ||= options.all?(&:index?)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
              end
         
     | 
| 
      
 12 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,13 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module LazyGraph
         
     | 
| 
      
 4 
     | 
    
         
            +
              module PathParser
         
     | 
| 
      
 5 
     | 
    
         
            +
                INDEX_REGEXP = /\A-?\d+\z/
         
     | 
| 
      
 6 
     | 
    
         
            +
                # Represents a single part of a path.
         
     | 
| 
      
 7 
     | 
    
         
            +
                PathPart = Struct.new(:part, keyword_init: true) do
         
     | 
| 
      
 8 
     | 
    
         
            +
                  def index?
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @index ||= part =~ INDEX_REGEXP
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
            end
         
     |