class Flows::Flow

Abstraction for build [deterministic finite-state machine](www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/)-like execution objects.

Let's refer to 'deterministic finite-state machine' as DFSM.

It's NOT an implementation of DFSM. It just shares a lot of structural ideas. You can also think about {Flow} as an [oriented graph](en.wikipedia.org/wiki/Graph_(discrete_mathematics)#Oriented_graph), where:

And edges formed by possible next nodes.

DFSM has a very important property:

> From any state, there is only one transition for any allowed input.

So, we represent DFSM states as {Node}s. Each {Node}, basing on input (input includes execution context also) performs some side effects and returns output and next {Node} name (DFSM state).

Side effects here can be spitted into two types:

Final state represented by special symbol `:end`.

@note You should not use {Flow} in your business code. It's designed to be underlying execution engine

for high-level abstractions. In other words - it's for libraries, not applications.

@example Calculates sum of elements in array. If sum more than 10 prints 'Big', otherwise prints 'Small'.

flow = Flows::Flow.new(
  start_node: :sum_list,
  node_map: {
    sum_list: Flows::Flow::Node.new(
      body: ->(list) { list.sum },
      router: Flows::Flow::Router::Custom.new(
        ->(x) { x > 10 } => :print_big,
        ->(x) { x <= 10 } => :print_small
      )
    ),
    print_big: Flows::Flow::Node.new(
      body: ->(_) { puts 'Big' },
      router: Flows::Flow::Router::Custom.new(
        nil => :end # puts returns nil.
      )
    ),
    print_small: Flows::Flow::Node.new(
      body: ->(_) { puts 'Small' },
      router: Flows::Flow::Router::Custom.new(
        nil => :end # puts returns nil.
      )
    )
  }
)

flow.call([1, 2, 3, 4, 5], context: {})
# prints 'Big' and returns nil

Public Class Methods

new(start_node:, node_map:) click to toggle source

@param start_node [Symbol] name of the entry node. @param node_map [Hash<Symbol, Node>] keys are node names, values are nodes. @raise [Flows::Flow::InvalidNodeRouteError] when some node has invalid routing destination. @raise [Flows::Flow::InvalidFirstNodeError] when first node is not presented in node map.

# File lib/flows/flow.rb, line 72
def initialize(start_node:, node_map:)
  @start_node = start_node
  @node_map = node_map

  check_routing_integrity
end

Public Instance Methods

call(input, context:) click to toggle source

Executes a flow.

@param input [Object] initial input @param context [Hash] mutable execution context @return [Object] execution result

# File lib/flows/flow.rb, line 84
def call(input, context:)
  current_node_name = @start_node

  while current_node_name != :end
    input, current_node_name = @node_map[current_node_name].call(input, context: context)
  end

  input
end

Private Instance Methods

check_node_routing_integrity(node_name, node) click to toggle source
# File lib/flows/flow.rb, line 102
def check_node_routing_integrity(node_name, node)
  node.router.destinations.each do |destination_name|
    if destination_name != :end && !@node_map.key?(destination_name)
      raise InvalidNodeRouteError.new(node_name, destination_name)
    end
  end
end
check_routing_integrity() click to toggle source
# File lib/flows/flow.rb, line 96
def check_routing_integrity
  raise InvalidFirstNodeError, @start_node unless @node_map.key?(@start_node)

  @node_map.each { |node_name, node| check_node_routing_integrity(node_name, node) }
end