Skip to content

jsmmr/ruby_callable_tree

Repository files navigation

CallableTree

build CodeQL

A framework for building tree-structured executable workflows in Ruby.

Construct trees of callable nodes to handle complex execution flows. Supports strategies to seek specific handlers, broadcast to multiple listeners, or compose processing pipelines. Nodes are matched against input and executed in a chain, offering a structured, modular alternative to complex conditional logic.

Installation

Add this line to your application's Gemfile:

gem 'callable_tree'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install callable_tree

Usage

Builds a tree of CallableTree nodes. Invokes the call method on nodes where match? returns a truthy value, chaining execution from root to leaf.

  • CallableTree::Node::Internal
    • Defines a node that can have child nodes. Supports several strategies (seekable, broadcastable, composable).
  • CallableTree::Node::External
    • Defines a leaf node, which cannot have child nodes.
  • CallableTree::Node::Root
    • Includes CallableTree::Node::Internal. Use this class when customization of the internal node is not required.

Basic

There are three ways to define nodes: class style, builder style, and factory style.

CallableTree::Node::Internal#seekable (default strategy)

This strategy stops processing subsequent sibling nodes if the current node's call method returns a non-nil value. This behavior is changeable by overriding the terminate? method.

Class style

examples/class/internal-seekable.rb:

module Node
  module JSON
    class Parser
      include CallableTree::Node::Internal

      def match?(input, **_options)
        File.extname(input) == '.json'
      end

      # Override `call` if you need to transform input values for child nodes.
      def call(input, **options)
        File.open(input) do |file|
          json = ::JSON.load(file)
          super(json, **options)
        end
      end

      # Override `terminate?` to return `true` to stop processing sibling nodes even if `call` returns `nil`.
      def terminate?(_output, *_inputs, **_options)
        true
      end
    end

    class Scraper
      include CallableTree::Node::External

      def initialize(type:)
        @type = type
      end

      def match?(input, **_options)
        !!input[@type.to_s]
      end

      def call(input, **_options)
        input[@type.to_s]
          .to_h { |element| [element['name'], element['emoji']] }
      end
    end
  end

  module XML
    class Parser
      include CallableTree::Node::Internal

      def match?(input, **_options)
        File.extname(input) == '.xml'
      end

      # Override `call` if you need to transform input values for child nodes.
      def call(input, **options)
        File.open(input) do |file|
          super(REXML::Document.new(file), **options)
        end
      end

      # Override `terminate?` to return `true` to stop processing sibling nodes even if `call` returns `nil`.
      def terminate?(_output, *_inputs, **_options)
        true
      end
    end

    class Scraper
      include CallableTree::Node::External

      def initialize(type:)
        @type = type
      end

      def match?(input, **_options)
        !input.get_elements("//#{@type}").empty?
      end

      def call(input, **_options)
        input
          .get_elements("//#{@type}")
          .first
          .to_h { |element| [element['name'], element['emoji']] }
      end
    end
  end
end

# The `seekable` call can be omitted as it is the default strategy.
tree = CallableTree::Node::Root.new.seekable.append(
  Node::JSON::Parser.new.seekable.append(
    Node::JSON::Scraper.new(type: :animals),
    Node::JSON::Scraper.new(type: :fruits)
  ),
  Node::XML::Parser.new.seekable.append(
    Node::XML::Scraper.new(type: :animals),
    Node::XML::Scraper.new(type: :fruits)
  )
)

Dir.glob("#{__dir__}/docs/*") do |file|
  options = { foo: :bar }
  pp tree.call(file, **options)
  puts '---'
end

Run examples/class/internal-seekable.rb:

% ruby examples/class/internal-seekable.rb
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---
Builder style

examples/builder/internal-seekable.rb:

JSONParser =
  CallableTree::Node::Internal::Builder
  .new
  .matcher do |input, **_options|
    File.extname(input) == '.json'
  end
  .caller do |input, **options, &original|
    File.open(input) do |file|
      json = ::JSON.load(file)
      # The following block call is equivalent to calling `super` in the class style.
      original.call(json, **options)
    end
  end
  .terminator { true }
  .build

XMLParser =
  CallableTree::Node::Internal::Builder
  .new
  .matcher do |input, **_options|
    File.extname(input) == '.xml'
  end
  .caller do |input, **options, &original|
    File.open(input) do |file|
      # The following block call is equivalent to calling `super` in the class style.
      original.call(REXML::Document.new(file), **options)
    end
  end
  .terminator { true }
  .build

def build_json_scraper(type)
  CallableTree::Node::External::Builder
    .new
    .matcher do |input, **_options|
      !!input[type.to_s]
    end
    .caller do |input, **_options|
      input[type.to_s]
        .to_h { |element| [element['name'], element['emoji']] }
    end
    .build
end

AnimalsJSONScraper = build_json_scraper(:animals)
FruitsJSONScraper = build_json_scraper(:fruits)

def build_xml_scraper(type)
  CallableTree::Node::External::Builder
    .new
    .matcher do |input, **_options|
      !input.get_elements("//#{type}").empty?
    end
    .caller do |input, **_options|
      input
        .get_elements("//#{type}")
        .first
        .to_h { |element| [element['name'], element['emoji']] }
    end
    .build
end

AnimalsXMLScraper = build_xml_scraper(:animals)
FruitsXMLScraper = build_xml_scraper(:fruits)

tree = CallableTree::Node::Root.new.seekable.append(
  JSONParser.new.seekable.append(
    AnimalsJSONScraper.new,
    FruitsJSONScraper.new
  ),
  XMLParser.new.seekable.append(
    AnimalsXMLScraper.new,
    FruitsXMLScraper.new
  )
)

Dir.glob("#{__dir__}/../docs/*") do |file|
  options = { foo: :bar }
  pp tree.call(file, **options)
  puts '---'
end

Run examples/builder/internal-seekable.rb:

% ruby examples/builder/internal-seekable.rb
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---
Factory style

Factory style defines behaviors as procs first, then assembles the tree structure separately. This makes the tree structure clearly visible.

examples/factory/internal-seekable.rb:

# === Behavior Definitions ===

json_matcher = ->(input, **) { File.extname(input) == '.json' }
json_caller = lambda do |input, **options, &original|
  File.open(input) do |file|
    json = JSON.parse(file.read)
    original.call(json, **options)
  end
end

xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
xml_caller = lambda do |input, **options, &original|
  File.open(input) do |file|
    original.call(REXML::Document.new(file), **options)
  end
end

animals_json_matcher = ->(input, **) { !input['animals'].nil? }
animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }

fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }

animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }

fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }

terminator_true = ->(*) { true }

# === Tree Structure (clearly visible!) ===

tree = CallableTree::Node::Root.new.seekable.append(
  CallableTree::Node::Internal.create(
    matcher: json_matcher,
    caller: json_caller,
    terminator: terminator_true
  ).seekable.append(
    CallableTree::Node::External.create(matcher: animals_json_matcher, caller: animals_json_caller),
    CallableTree::Node::External.create(matcher: fruits_json_matcher, caller: fruits_json_caller)
  ),
  CallableTree::Node::Internal.create(
    matcher: xml_matcher,
    caller: xml_caller,
    terminator: terminator_true
  ).seekable.append(
    CallableTree::Node::External.create(matcher: animals_xml_matcher, caller: animals_xml_caller),
    CallableTree::Node::External.create(matcher: fruits_xml_matcher, caller: fruits_xml_caller)
  )
)

Dir.glob("#{__dir__}/../docs/*") do |file|
  options = { foo: :bar }
  pp tree.call(file, **options)
  puts '---'
end

Run examples/factory/internal-seekable.rb:

% ruby examples/factory/internal-seekable.rb
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Dog"=>"🐢", "Cat"=>"🐱"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
---

CallableTree::Node::Internal#broadcastable

This strategy broadcasts input to all child nodes and returns their results as an array. It ignores child terminate? methods by default.

Class style

examples/class/internal-broadcastable.rb:

module Node
  class LessThan
    include CallableTree::Node::Internal

    def initialize(num)
      @num = num
    end

    def match?(input)
      super && input < @num
    end
  end
end

tree = CallableTree::Node::Root.new.broadcastable.append(
  Node::LessThan.new(5).broadcastable.append(
    ->(input) { input * 2 }, # anonymous external node
    ->(input) { input + 1 }  # anonymous external node
  ),
  Node::LessThan.new(10).broadcastable.append(
    ->(input) { input * 3 }, # anonymous external node
    ->(input) { input - 1 }  # anonymous external node
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/class/internal-broadcastable.rb:

% ruby examples/class/internal-broadcastable.rb
0 -> [[0, 1], [0, -1]]
1 -> [[2, 2], [3, 0]]
2 -> [[4, 3], [6, 1]]
3 -> [[6, 4], [9, 2]]
4 -> [[8, 5], [12, 3]]
5 -> [nil, [15, 4]]
6 -> [nil, [18, 5]]
7 -> [nil, [21, 6]]
8 -> [nil, [24, 7]]
9 -> [nil, [27, 8]]
10 -> [nil, nil]
Builder style

examples/builder/internal-broadcastable.rb:

def less_than(num)
  # The following block call is equivalent to calling `super` in the class style.
  proc { |input, &original| original.call(input) && input < num }
end

LessThan5 =
  CallableTree::Node::Internal::Builder
  .new
  .matcher(&method(:less_than).call(5))
  .build

LessThan10 =
  CallableTree::Node::Internal::Builder
  .new
  .matcher(&method(:less_than).call(10))
  .build

def add(num)
  proc { |input| input + num }
end

Add1 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:add).call(1))
  .build

def subtract(num)
  proc { |input| input - num }
end

Subtract1 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:subtract).call(1))
  .build

def multiply(num)
  proc { |input| input * num }
end

Multiply2 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:multiply).call(2))
  .build

Multiply3 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:multiply).call(3))
  .build

tree = CallableTree::Node::Root.new.broadcastable.append(
  LessThan5.new.broadcastable.append(
    Multiply2.new,
    Add1.new
  ),
  LessThan10.new.broadcastable.append(
    Multiply3.new,
    Subtract1.new
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/builder/internal-broadcastable.rb:

% ruby examples/builder/internal-broadcastable.rb
0 -> [[0, 1], [0, -1]]
1 -> [[2, 2], [3, 0]]
2 -> [[4, 3], [6, 1]]
3 -> [[6, 4], [9, 2]]
4 -> [[8, 5], [12, 3]]
5 -> [nil, [15, 4]]
6 -> [nil, [18, 5]]
7 -> [nil, [21, 6]]
8 -> [nil, [24, 7]]
9 -> [nil, [27, 8]]
10 -> [nil, nil]
Factory style

examples/factory/internal-broadcastable.rb:

# === Behavior Definitions ===

less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }

multiply_2_caller = ->(input, **) { input * 2 }
add_1_caller = ->(input, **) { input + 1 }
multiply_3_caller = ->(input, **) { input * 3 }
subtract_1_caller = ->(input, **) { input - 1 }

# === Tree Structure ===

tree = CallableTree::Node::Root.new.broadcastable.append(
  CallableTree::Node::Internal.create(matcher: less_than_5_matcher).broadcastable.append(
    CallableTree::Node::External.create(caller: multiply_2_caller),
    CallableTree::Node::External.create(caller: add_1_caller)
  ),
  CallableTree::Node::Internal.create(matcher: less_than_10_matcher).broadcastable.append(
    CallableTree::Node::External.create(caller: multiply_3_caller),
    CallableTree::Node::External.create(caller: subtract_1_caller)
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/factory/internal-broadcastable.rb:

% ruby examples/factory/internal-broadcastable.rb
0 -> [[0, 1], [0, -1]]
1 -> [[2, 2], [3, 0]]
2 -> [[4, 3], [6, 1]]
3 -> [[6, 4], [9, 2]]
4 -> [[8, 5], [12, 3]]
5 -> [nil, [15, 4]]
6 -> [nil, [18, 5]]
7 -> [nil, [21, 6]]
8 -> [nil, [24, 7]]
9 -> [nil, [27, 8]]
10 -> [nil, nil]

CallableTree::Node::Internal#composable

This strategy chains child nodes, passing the output of the previous node as input to the next. It also ignores their terminate? methods by default.

Class style

examples/class/internal-composable.rb:

module Node
  class LessThan
    include CallableTree::Node::Internal

    def initialize(num)
      @num = num
    end

    def match?(input)
      super && input < @num
    end
  end
end

tree = CallableTree::Node::Root.new.composable.append(
  Node::LessThan.new(5).composable.append(
    proc { |input| input * 2 }, # anonymous external node
    proc { |input| input + 1 }  # anonymous external node
  ),
  Node::LessThan.new(10).composable.append(
    proc { |input| input * 3 }, # anonymous external node
    proc { |input| input - 1 }  # anonymous external node
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/class/internal-composable.rb:

% ruby examples/class/internal-composable.rb
0 -> 2
1 -> 8
2 -> 14
3 -> 20
4 -> 26
5 -> 14
6 -> 17
7 -> 20
8 -> 23
9 -> 26
10 -> 10
Builder style

examples/builder/internal-composable.rb:

def less_than(num)
  # The following block call is equivalent to calling `super` in the class style.
  proc { |input, &original| original.call(input) && input < num }
end

LessThan5 =
  CallableTree::Node::Internal::Builder
  .new
  .matcher(&method(:less_than).call(5))
  .build

LessThan10 =
  CallableTree::Node::Internal::Builder
  .new
  .matcher(&method(:less_than).call(10))
  .build

def add(num)
  proc { |input| input + num }
end

Add1 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:add).call(1))
  .build

def subtract(num)
  proc { |input| input - num }
end

Subtract1 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:subtract).call(1))
  .build

def multiply(num)
  proc { |input| input * num }
end

Multiply2 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:multiply).call(2))
  .build

Multiply3 =
  CallableTree::Node::External::Builder
  .new
  .caller(&method(:multiply).call(3))
  .build

tree = CallableTree::Node::Root.new.composable.append(
  LessThan5.new.composable.append(
    Multiply2.new,
    Add1.new
  ),
  LessThan10.new.composable.append(
    Multiply3.new,
    Subtract1.new
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/builder/internal-composable.rb:

% ruby examples/builder/internal-composable.rb
0 -> 2
1 -> 8
2 -> 14
3 -> 20
4 -> 26
5 -> 14
6 -> 17
7 -> 20
8 -> 23
9 -> 26
10 -> 10
Factory style

examples/factory/internal-composable.rb:

# === Behavior Definitions ===

less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }

multiply_2_caller = ->(input, **) { input * 2 }
add_1_caller = ->(input, **) { input + 1 }
multiply_3_caller = ->(input, **) { input * 3 }
subtract_1_caller = ->(input, **) { input - 1 }

# === Tree Structure ===

tree = CallableTree::Node::Root.new.composable.append(
  CallableTree::Node::Internal.create(matcher: less_than_5_matcher).composable.append(
    CallableTree::Node::External.create(caller: multiply_2_caller),
    CallableTree::Node::External.create(caller: add_1_caller)
  ),
  CallableTree::Node::Internal.create(matcher: less_than_10_matcher).composable.append(
    CallableTree::Node::External.create(caller: multiply_3_caller),
    CallableTree::Node::External.create(caller: subtract_1_caller)
  )
)

(0..10).each do |input|
  output = tree.call(input)
  puts "#{input} -> #{output}"
end

Run examples/factory/internal-composable.rb:

% ruby examples/factory/internal-composable.rb
0 -> 2
1 -> 8
2 -> 14
3 -> 20
4 -> 26
5 -> 14
6 -> 17
7 -> 20
8 -> 23
9 -> 26
10 -> 10

Advanced

CallableTree::Node::External#verbosify

Use this method to enable verbose output.

examples/builder/external-verbosify.rb:

...

tree = CallableTree::Node::Root.new.seekable.append(
  JSONParser.new.seekable.append(
    AnimalsJSONScraper.new.verbosify,
    FruitsJSONScraper.new.verbosify
  ),
  XMLParser.new.seekable.append(
    AnimalsXMLScraper.new.verbosify,
    FruitsXMLScraper.new.verbosify
  )
)

...

Run examples/builder/external-verbosify.rb:

% ruby examples/class/external-verbosify.rb
#<struct CallableTree::Node::External::Output
  value={"Dog"=>"🐢", "Cat"=>"🐱"},
  options={:foo=>:bar},
  routes=[AnimalsJSONScraper, JSONParser, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
  value={"Dog"=>"🐢", "Cat"=>"🐱"},
  options={:foo=>:bar},
  routes=[AnimalsXMLScraper, XMLParser, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
  value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
  options={:foo=>:bar},
  routes=[FruitsJSONScraper, JSONParser, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
  value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
  options={:foo=>:bar},
  routes=[FruitsXMLScraper, XMLParser, CallableTree::Node::Root]>
---

Logging

This is an example of logging.

examples/builder/logging.rb:

JSONParser =
  CallableTree::Node::Internal::Builder
  .new
  ...
  .hookable
  .build

XMLParser =
  CallableTree::Node::Internal::Builder
  .new
  ...
  .hookable
  .build

def build_json_scraper(type)
  CallableTree::Node::External::Builder
    .new
    ...
    .hookable
    .build
end

...

def build_xml_scraper(type)
  CallableTree::Node::External::Builder
    .new
    ...
    .hookable
    .build
end

...

module Logging
  INDENT_SIZE = 2
  BLANK = ' '
  LIST_STYLE = '*'
  INPUT_LABEL  = 'Input :'
  OUTPUT_LABEL = 'Output:'

  def self.loggable(node)
    node.after_matcher! do |matched, _node_:, **|
      prefix = LIST_STYLE.rjust((_node_.depth * INDENT_SIZE) - INDENT_SIZE + LIST_STYLE.length, BLANK)
      puts "#{prefix} #{_node_.identity}: [matched: #{matched}]"
      matched
    end

    return unless node.external?

    node
      .before_caller! do |input, *, _node_:, **|
        input_prefix = INPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + INPUT_LABEL.length, BLANK)
        puts "#{input_prefix} #{input}"
        input
      end
      .after_caller! do |output, _node_:, **|
        output_prefix = OUTPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + OUTPUT_LABEL.length, BLANK)
        puts "#{output_prefix} #{output}"
        output
      end
  end
end

loggable = Logging.method(:loggable)

tree = CallableTree::Node::Root.new.seekable.append(
  JSONParser.new.tap(&loggable).seekable.append(
    AnimalsJSONScraper.new.tap(&loggable).verbosify,
    FruitsJSONScraper.new.tap(&loggable).verbosify
  ),
  XMLParser.new.tap(&loggable).seekable.append(
    AnimalsXMLScraper.new.tap(&loggable).verbosify,
    FruitsXMLScraper.new.tap(&loggable).verbosify
  )
)

...

Also, see examples/builder/hooks.rb for detail about CallableTree::Node::Hooks::*.

Run examples/builder/logging.rb:

% ruby examples/builder/logging.rb
* JSONParser: [matched: true]
  * AnimalsJSONScraper: [matched: true]
    Input : {"animals"=>[{"name"=>"Dog", "emoji"=>"🐢"}, {"name"=>"Cat", "emoji"=>"🐱"}]}
    Output: {"Dog"=>"🐢", "Cat"=>"🐱"}
#<struct CallableTree::Node::External::Output
 value={"Dog"=>"🐢", "Cat"=>"🐱"},
 options={:foo=>:bar},
 routes=[AnimalsJSONScraper, JSONParser, CallableTree::Node::Root]>
---
* JSONParser: [matched: false]
* XMLParser: [matched: true]
  * AnimalsXMLScraper: [matched: true]
    Input : <root><animals><animal emoji='🐢' name='Dog'/><animal emoji='🐱' name='Cat'/></animals></root>
    Output: {"Dog"=>"🐢", "Cat"=>"🐱"}
#<struct CallableTree::Node::External::Output
 value={"Dog"=>"🐢", "Cat"=>"🐱"},
 options={:foo=>:bar},
 routes=[AnimalsXMLScraper, XMLParser, CallableTree::Node::Root]>
---
* JSONParser: [matched: true]
  * AnimalsJSONScraper: [matched: false]
  * FruitsJSONScraper: [matched: true]
    Input : {"fruits"=>[{"name"=>"Red Apple", "emoji"=>"🍎"}, {"name"=>"Green Apple", "emoji"=>"🍏"}]}
    Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
#<struct CallableTree::Node::External::Output
 value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
 options={:foo=>:bar},
 routes=[FruitsJSONScraper, JSONParser, CallableTree::Node::Root]>
---
* JSONParser: [matched: false]
* XMLParser: [matched: true]
  * AnimalsXMLScraper: [matched: false]
  * FruitsXMLScraper: [matched: true]
    Input : <root><fruits><fruit emoji='🍎' name='Red Apple'/><fruit emoji='🍏' name='Green Apple'/></fruits></root>
    Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
#<struct CallableTree::Node::External::Output
 value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
 options={:foo=>:bar},
 routes=[FruitsXMLScraper, XMLParser, CallableTree::Node::Root]>

CallableTree::Node#identity

Specify an identifier to customize the node identity.

examples/builder/identity.rb:

JSONParser =
  CallableTree::Node::Internal::Builder
  .new
  ...
  .identifier { |_node_:| _node_.object_id }
  .build

XMLParser =
  CallableTree::Node::Internal::Builder
  .new
  ...
  .identifier { |_node_:| _node_.object_id }
  .build

def build_json_scraper(type)
  CallableTree::Node::External::Builder
    .new
    ...
    .identifier { |_node_:| _node_.object_id }
    .build
end

...

def build_xml_scraper(type)
  CallableTree::Node::External::Builder
    .new
    ...
    .identifier { |_node_:| _node_.object_id }
    .build
end

...

Run examples/builder/identity.rb:

 % ruby examples/builder/identity.rb
#<struct CallableTree::Node::External::Output
  value={"Dog"=>"🐢", "Cat"=>"🐱"},
  options={:foo=>:bar},
  routes=[60, 80, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
  value={"Dog"=>"🐢", "Cat"=>"🐱"},
  options={:foo=>:bar},
  routes=[220, 240, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
 value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
 options={:foo=>:bar},
 routes=[260, 80, CallableTree::Node::Root]>
---
#<struct CallableTree::Node::External::Output
 value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
 options={:foo=>:bar},
 routes=[400, 240, CallableTree::Node::Root]>
---

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/jsmmr/ruby_callable_tree.

License

The gem is available as open source under the terms of the MIT License.

About

A framework for building tree-structured executable workflows in Ruby.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •