class Flows::Flow::Node
Node
is the main building block for {Flow}.
Node
is an object which can be executed ({#call}) with some input and execution context and produce output and the next route.
Node
execution consists of 4 sequential parts:
-
Pre-processor execution (if pre-processor defined).
Allows to modify input which will be transferred to the node's body. Allows to modify execution context. Has access to the node's metadata.
-
Body execution. Body is a lambda which receives input and returns output.
-
Post-processor execution (if post-processor defined).
Allows to modify output which was produced by the node's body. Allows to modify execution context. Has access to the node's metadata.
-
{Router} execution to determine next node name.
Execution result consists of 2 parts: output and next route.
## Pre/postprocessors
Both have similar signatures:
preprocessor = lambda do |node_input, context, meta| # returns input for the BODY # format [args, kwargs] end postprocessor = lambda do |body_output, context, meta| # returns output of the NODE end
Types for body input and `body_output` is under your control. It allows you to adopt node for very different kinds of bodies.
Without pre-processor `input` from {#call} becomes the first and single argument for the body. In the cases when your body expects several arguments or keyword arguments you must use pre-processor. Pre-processor returns array of 2 elements - arguments and keyword arguments. There are some examples of a post-processor return and the corresponding body call:
[[1, 2], {}] body.call(1, 2) [[], { a: 1, b: 2}] body.call(a: 1, b: 2) [[1, 2], { x: 3, y: 4 }] body.call(1, 2, x: 3, y: 4) [[1, 2, { 'a' => 3}], {}] body.call(1, 2, 'a' => 3)
There were simpler solutions, but after [this](www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/) it's the most clear one.
`context` and `meta` are expected to be Ruby Hashes.
-
`context` - is an execution context and it's mutable. Execution context is defined outside and not controlled by a node. Therefore, your mutations will be visible after a node execution.
-
`meta` - is node execution metadata and it's immutable, read only and node-local. Designed to store purely technical information like node name or, maybe, some dependency injection entities.
## Metadata and node names
As you may see any Node
instance has no name. It's done for the reason: name is not part of a node; it allows to use the same node instance under different names. In the most cases we don't need this, but it's nice to have such ability.
In some cases we want to have a node name inside pre/postprocessors. For such cases `meta` is the right place to store node name:
Flows::Flow::Node.new(body: body, router: router, meta: { name: :step_a })
@see Flows::Flow
some examples here
Attributes
Node
metadata, a frozen Ruby Hash.
Public Class Methods
@param body [Proc] node body @param router [Router] node router @param meta [Hash] node metadata @param preprocessor [Proc, nil] pre-processor for the node's body @param postprocessor [Proc, nil] post-processor for the node's body
# File lib/flows/flow/node.rb, line 92 def initialize(body:, router:, meta: {}, preprocessor: nil, postprocessor: nil) @body = body @router = router @meta = meta.freeze @preprocessor = preprocessor @postprocessor = postprocessor end
Public Instance Methods
Executes the node.
@param input [Object] input for a node. In the context of {Flow}
it's initial input or output of the previously executed node.
@param context [Hash] execution context. In case of {Flow}
shared between node executions.
@return [Array<(Object, Symbol)>] output of a node and next route.
`:reek:TooManyStatements` is disabled for this method because even one more call to a private method impacts performance here.
# File lib/flows/flow/node.rb, line 114 def call(input, context:) output = if @preprocessor args, kwargs = @preprocessor.call(input, context, @meta) # https://bugs.ruby-lang.org/issues/14415 kwargs.empty? ? @body.call(*args) : @body.call(*args, **kwargs) else @body.call(input) end output = @postprocessor.call(output, context, @meta) if @postprocessor route = @router.call(output) [output, route] end