Module: Tins::DeepTransform

Included in:
HashSymbolizeKeysRecursive
Defined in:
lib/tins/deep_transform.rb

Overview

DeepTransform provides a robust engine for traversing an object tree and applying a transformation to each node iteratively, preventing stack overflows.

Instance Method Summary collapse

Instance Method Details

#_transform_iterative(root, key: nil, value: nil, circular:) ⇒ Object (private)

Note:

This method relies on to_hash and to_ary to identify container nodes. Objects must implement these methods if they are to be treated as branch nodes in the tree.

The core iterative engine used to traverse and transform object trees.

This method implements a non-recursive, two-pass approach to avoid SystemStackError on deeply nested structures:

  1. Discovery Pass: Performs a pre-order traversal using an explicit stack to identify all reachable nodes in the tree, tracking them by their object ID (__id__) to handle shared references and circularities.
  2. Reconstruction Pass: Processes the discovered nodes in reverse order (bottom-up). This ensures that child nodes are transformed and stored in the results map before their parent nodes are processed.

Parameters:

  • root (Object)

    The root object of the tree to be transformed.

  • key (Proc, nil) (defaults to: nil)

    An optional lambda/proc used to transform hash keys during reconstruction. Defaults to an identity transformation.

  • value (Proc, nil) (defaults to: nil)

    An optional lambda/proc used to transform nodes (values) during reconstruction. Its behavior depends on its arity:

    • Arity 1: called with (node)
    • Arity 2: called with (index_or_key, node)
    • Arity 3: called with (index_or_key, node, parent_container) Defaults to an identity transformation.
  • circular (Object)

    The value to assign when a circular reference is detected during the reconstruction phase.

Returns:

  • (Object)

    The root of the newly reconstructed and transformed tree.



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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/tins/deep_transform.rb', line 56

def _transform_iterative(root, key: nil, value: nil, circular:)
  key   ||= -> x { x }
  value ||= -> x { x }

  # Pass 1: Discovery (Pre-order traversal to find all nodes)
  nodes   = []
  stack   = [ [ nil, root, nil ] ]
  visited = {}

  while stack.any?
    idx, node, cont = stack.pop
    next if visited[node.__id__]

    visited[node.__id__] = true
    nodes << [ idx, node, cont ]

    # Unfold containers to find children for discovery
    if node.respond_to?(:to_hash)
      node.to_hash.each_pair { |k, v| stack << [ k, v, node ] }
    elsif node.respond_to?(:to_ary)
      node.to_ary.each_with_index { |v, i| stack << [ i, v, node ] }
    end
  end

  # Pass 2: Bottom-Up Reconstruction
  results = {}
  nodes.reverse_each do |idx, node, cont|
    transformed = case value.arity
                  when 1 then value.(node)
                  when 2 then value.(idx, node)
                  when 3 then value.(idx, node, cont)
                  else
                    raise ArgumentError, 'value lambda has to be arity in 1..3'
                  end

    if transformed.respond_to?(:to_hash)
      hash = transformed.to_hash
      new_hash = hash.dup.tap(&:clear)
      hash.each do |k, v|
        # Resolve child from results map or mark as circular
        new_hash[key.(k)] = results[v.__id__] ||
          (v.respond_to?(:to_hash) || v.respond_to?(:to_ary) ? circular : v)
      end
      transformed = new_hash
    elsif transformed.respond_to?(:to_ary)
      ary = transformed.to_ary
      transformed = ary.map do |v|
        results[v.__id__] ||
          (v.respond_to?(:to_hash) || v.respond_to?(:to_ary) ? circular : v)
      end
    end

    results[node.__id__] = transformed
  end

  results[root.__id__]
end

#deep_transform(key: nil, value: nil, circular: nil) ⇒ Object

Transforms an object and its children using the provided lambdas. This implementation uses an iterative bottom-up approach to avoid SystemStackError.

Parameters:

  • key (Proc, nil) (defaults to: nil)

    An optional lambda/proc used to transform hash keys during reconstruction. Defaults to an identity transformation.

  • value (Proc, nil) (defaults to: nil)

    An optional lambda/proc used to transform nodes (values) during reconstruction. Its behavior depends on its arity:

    • Arity 1: called with (node)
    • Arity 2: called with (index_or_key, node)
    • Arity 3: called with (index_or_key, node, parent_container) Defaults to an identity transformation.
  • circular (Object) (defaults to: nil)

    value returned when a circular reference is detected

Returns:

  • (Object)

    the fully transformed object tree

Raises:

  • (ArgumentError)

    if a block is provided (use key: and value: instead)



21
22
23
24
# File 'lib/tins/deep_transform.rb', line 21

def deep_transform(key: nil, value: nil, circular: nil)
  block_given? and raise ArgumentError, '&block not supported'
  _transform_iterative(self, key: key, value: value, circular: circular)
end