flows 0.2.0 → 0.6.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 +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +9 -1
- data/.mdlrc +1 -1
- data/.reek.yml +54 -0
- data/.rubocop.yml +26 -7
- data/.rubocop_todo.yml +27 -0
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +0 -6
- data/README.md +167 -363
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +73 -105
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +138 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/interface_error_demo.rb +17 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/README.md +3 -187
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +27 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +24 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +15 -0
- data/lib/flows/plugin/dependency_injector.rb +170 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/interface.rb +84 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -34
- data/lib/flows/railway/dsl.rb +8 -18
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +158 -16
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +342 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +55 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +191 -0
- data/lib/flows/version.rb +1 -1
- metadata +253 -38
- data/Gemfile.lock +0 -174
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -52
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -0,0 +1,70 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Makes a contract fixed size array.
|
4
|
+
#
|
5
|
+
# Underlying contracts' transformations are applied.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# name_age = Flows::Contract::Tuple.new(String, Integer)
|
9
|
+
#
|
10
|
+
# name_age === ['Roman', 29]
|
11
|
+
# # => true
|
12
|
+
#
|
13
|
+
# name_age === [10, 20]
|
14
|
+
# # => false
|
15
|
+
class Tuple < Contract
|
16
|
+
ARRAY_CONTRACT = CaseEq.new(::Array)
|
17
|
+
|
18
|
+
# @param contracts [Array<Contract, Object>] contract list. {CaseEq} applied to non-contract values.
|
19
|
+
def initialize(*contracts)
|
20
|
+
@contracts = contracts.map(&method(:to_contract))
|
21
|
+
end
|
22
|
+
|
23
|
+
# @see Contract#check!
|
24
|
+
def check!(other)
|
25
|
+
ARRAY_CONTRACT.check!(other)
|
26
|
+
check_length(other)
|
27
|
+
|
28
|
+
errors = collect_errors(other)
|
29
|
+
return true if errors.empty?
|
30
|
+
|
31
|
+
raise Error.new(other, render_errors(other, errors))
|
32
|
+
end
|
33
|
+
|
34
|
+
# @see Contract#transform!
|
35
|
+
def transform!(other)
|
36
|
+
check!(other)
|
37
|
+
|
38
|
+
other.map.with_index do |elem, index|
|
39
|
+
@contracts[index].transform!(elem)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def check_length(other)
|
46
|
+
return if other.length == @contracts.length
|
47
|
+
|
48
|
+
raise Error.new(other, "array length mismatch: must be #{@contracts.length}, got #{other.length}")
|
49
|
+
end
|
50
|
+
|
51
|
+
def collect_errors(other)
|
52
|
+
other.each_with_object({}).with_index do |(elem, errors), index|
|
53
|
+
result = @contracts[index].check(elem)
|
54
|
+
|
55
|
+
errors[index] = result.error if result.err?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def render_errors(other, errors)
|
60
|
+
errors.map do |index, err|
|
61
|
+
elem = other[index]
|
62
|
+
merge_nested_errors(
|
63
|
+
"array element `#{elem.inspect}` with index #{index} is invalid:",
|
64
|
+
err
|
65
|
+
)
|
66
|
+
end.join("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/flows/flow.rb
CHANGED
@@ -1,21 +1,110 @@
|
|
1
|
+
require_relative 'flow/errors'
|
2
|
+
require_relative 'flow/node'
|
3
|
+
require_relative 'flow/router'
|
4
|
+
|
1
5
|
module Flows
|
2
|
-
#
|
6
|
+
# Abstraction for build [deterministic finite-state machine](https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/)-like
|
7
|
+
# execution objects.
|
8
|
+
#
|
9
|
+
# Let's refer to 'deterministic finite-state machine' as DFSM.
|
10
|
+
#
|
11
|
+
# It's NOT an implementation of DFSM. It just shares a lot of
|
12
|
+
# structural ideas. You can also think about {Flow} as an [oriented graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)#Oriented_graph),
|
13
|
+
# where:
|
14
|
+
#
|
15
|
+
# * you have the one and only one initial node
|
16
|
+
# * you start execution from the initial node
|
17
|
+
# * after node execution your are going to some next node or stop execution.
|
18
|
+
#
|
19
|
+
# And edges formed by possible next nodes.
|
20
|
+
#
|
21
|
+
# DFSM has a very important property:
|
22
|
+
#
|
23
|
+
# > From any state, there is only one transition for any allowed input.
|
24
|
+
#
|
25
|
+
# So, we represent DFSM states as {Node}s. Each {Node}, basing on input (input includes execution context also)
|
26
|
+
# performs some side effects and returns output and next {Node} name (DFSM state).
|
27
|
+
#
|
28
|
+
# Side effects here can be spitted into two types:
|
29
|
+
#
|
30
|
+
# * modification of execution context
|
31
|
+
# * rest of them: working with 3rd party libraries, printing to STDOUT, etc.
|
32
|
+
#
|
33
|
+
# Final state represented by special symbol `:end`.
|
34
|
+
#
|
35
|
+
# @note You should not use {Flow} in your business code. It's designed to be underlying execution engine
|
36
|
+
# for high-level abstractions. In other words - it's for libraries, not applications.
|
37
|
+
#
|
38
|
+
# @example Calculates sum of elements in array. If sum more than 10 prints 'Big', otherwise prints 'Small'.
|
39
|
+
#
|
40
|
+
# flow = Flows::Flow.new(
|
41
|
+
# start_node: :sum_list,
|
42
|
+
# node_map: {
|
43
|
+
# sum_list: Flows::Flow::Node.new(
|
44
|
+
# body: ->(list) { list.sum },
|
45
|
+
# router: Flows::Flow::Router::Custom.new(
|
46
|
+
# ->(x) { x > 10 } => :print_big,
|
47
|
+
# ->(x) { x <= 10 } => :print_small
|
48
|
+
# )
|
49
|
+
# ),
|
50
|
+
# print_big: Flows::Flow::Node.new(
|
51
|
+
# body: ->(_) { puts 'Big' },
|
52
|
+
# router: Flows::Flow::Router::Custom.new(
|
53
|
+
# nil => :end # puts returns nil.
|
54
|
+
# )
|
55
|
+
# ),
|
56
|
+
# print_small: Flows::Flow::Node.new(
|
57
|
+
# body: ->(_) { puts 'Small' },
|
58
|
+
# router: Flows::Flow::Router::Custom.new(
|
59
|
+
# nil => :end # puts returns nil.
|
60
|
+
# )
|
61
|
+
# )
|
62
|
+
# }
|
63
|
+
# )
|
64
|
+
#
|
65
|
+
# flow.call([1, 2, 3, 4, 5], context: {})
|
66
|
+
# # prints 'Big' and returns nil
|
3
67
|
class Flow
|
4
|
-
|
68
|
+
# @param start_node [Symbol] name of the entry node.
|
69
|
+
# @param node_map [Hash<Symbol, Node>] keys are node names, values are nodes.
|
70
|
+
# @raise [Flows::Flow::InvalidNodeRouteError] when some node has invalid routing destination.
|
71
|
+
# @raise [Flows::Flow::InvalidFirstNodeError] when first node is not presented in node map.
|
72
|
+
def initialize(start_node:, node_map:)
|
5
73
|
@start_node = start_node
|
6
|
-
@
|
7
|
-
|
8
|
-
|
74
|
+
@node_map = node_map
|
75
|
+
|
76
|
+
check_routing_integrity
|
9
77
|
end
|
10
78
|
|
79
|
+
# Executes a flow.
|
80
|
+
#
|
81
|
+
# @param input [Object] initial input
|
82
|
+
# @param context [Hash] mutable execution context
|
83
|
+
# @return [Object] execution result
|
11
84
|
def call(input, context:)
|
12
85
|
current_node_name = @start_node
|
13
86
|
|
14
|
-
while current_node_name != :
|
15
|
-
input, current_node_name = @
|
87
|
+
while current_node_name != :end
|
88
|
+
input, current_node_name = @node_map[current_node_name].call(input, context: context)
|
16
89
|
end
|
17
90
|
|
18
91
|
input
|
19
92
|
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def check_routing_integrity
|
97
|
+
raise InvalidFirstNodeError, @start_node unless @node_map.key?(@start_node)
|
98
|
+
|
99
|
+
@node_map.each { |node_name, node| check_node_routing_integrity(node_name, node) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_node_routing_integrity(node_name, node)
|
103
|
+
node.router.destinations.each do |destination_name|
|
104
|
+
if destination_name != :end && !@node_map.key?(destination_name)
|
105
|
+
raise InvalidNodeRouteError.new(node_name, destination_name)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
20
109
|
end
|
21
110
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Flows
|
2
|
+
class Flow
|
3
|
+
# Base class for {Flow} error
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Raised when router has an impossible route.
|
7
|
+
class InvalidNodeRouteError < Error
|
8
|
+
def initialize(node_name, route_destination)
|
9
|
+
@node_name = node_name.inspect
|
10
|
+
@route_destination = route_destination.inspect
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
"Node `#{@node_name}` has a route to `#{@route_destination}`, but node `#{@route_destination}` is missing."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Raised when router has an impossible route.
|
19
|
+
class InvalidFirstNodeError < Error
|
20
|
+
def initialize(node_name)
|
21
|
+
@node_name = node_name.inspect
|
22
|
+
end
|
23
|
+
|
24
|
+
def message
|
25
|
+
"`#{@node_name}` is a first node name, but node `#{@node_name}` is missing."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Flows
|
2
|
+
class Flow
|
3
|
+
# Node is the main building block for {Flow}.
|
4
|
+
#
|
5
|
+
# Node is an object which can be executed ({#call}) with some input and execution context
|
6
|
+
# and produce output and the next route.
|
7
|
+
#
|
8
|
+
# Node execution consists of 4 sequential parts:
|
9
|
+
#
|
10
|
+
# 1. Pre-processor execution (if pre-processor defined).
|
11
|
+
# Allows to modify input which will be transferred to the node's body.
|
12
|
+
# Allows to modify execution context.
|
13
|
+
# Has access to the node's metadata.
|
14
|
+
# 2. Body execution. Body is a lambda which receives input and returns output.
|
15
|
+
# 3. Post-processor execution (if post-processor defined).
|
16
|
+
# Allows to modify output which was produced by the node's body.
|
17
|
+
# Allows to modify execution context.
|
18
|
+
# Has access to the node's metadata.
|
19
|
+
# 4. {Router} execution to determine next node name.
|
20
|
+
#
|
21
|
+
# Execution result consists of 2 parts: output and next route.
|
22
|
+
#
|
23
|
+
# ## Pre/postprocessors
|
24
|
+
#
|
25
|
+
# Both have similar signatures:
|
26
|
+
#
|
27
|
+
# preprocessor = lambda do |node_input, context, meta|
|
28
|
+
# # returns input for the BODY
|
29
|
+
# # format [args, kwargs]
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# postprocessor = lambda do |body_output, context, meta|
|
33
|
+
# # returns output of the NODE
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Types for body input and `body_output` is under your control.
|
37
|
+
# It allows you to adopt node for very different kinds of bodies.
|
38
|
+
#
|
39
|
+
# Without pre-processor `input` from {#call} becomes the first and single argument for the body.
|
40
|
+
# In the cases when your body expects several arguments or keyword arguments
|
41
|
+
# you must use pre-processor. Pre-processor returns array of 2 elements -
|
42
|
+
# arguments and keyword arguments. There are some examples of a post-processor return
|
43
|
+
# and the corresponding body call:
|
44
|
+
#
|
45
|
+
# [[1, 2], {}]
|
46
|
+
# body.call(1, 2)
|
47
|
+
#
|
48
|
+
# [[], { a: 1, b: 2}]
|
49
|
+
# body.call(a: 1, b: 2)
|
50
|
+
#
|
51
|
+
# [[1, 2], { x: 3, y: 4 }]
|
52
|
+
# body.call(1, 2, x: 3, y: 4)
|
53
|
+
#
|
54
|
+
# [[1, 2, { 'a' => 3}], {}]
|
55
|
+
# body.call(1, 2, 'a' => 3)
|
56
|
+
#
|
57
|
+
# There were simpler solutions, but after [this](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
|
58
|
+
# it's the most clear one.
|
59
|
+
#
|
60
|
+
# `context` and `meta` are expected to be Ruby Hashes.
|
61
|
+
#
|
62
|
+
# * `context` - is an execution context and it's mutable.
|
63
|
+
# Execution context is defined outside and not controlled by a node.
|
64
|
+
# Therefore, your mutations will be visible after a node execution.
|
65
|
+
# * `meta` - is node execution metadata and it's immutable, read only and node-local.
|
66
|
+
# Designed to store purely technical information like
|
67
|
+
# node name or, maybe, some dependency injection entities.
|
68
|
+
#
|
69
|
+
# ## Metadata and node names
|
70
|
+
#
|
71
|
+
# As you may see any Node instance has no name.
|
72
|
+
# It's done for the reason: name is not part of a node;
|
73
|
+
# it allows to use the same node instance under different names.
|
74
|
+
# In the most cases we don't need this, but it's nice to have such ability.
|
75
|
+
#
|
76
|
+
# In some cases we want to have a node name inside pre/postprocessors.
|
77
|
+
# For such cases `meta` is the right place to store node name:
|
78
|
+
#
|
79
|
+
# Flows::Flow::Node.new(body: body, router: router, meta: { name: :step_a })
|
80
|
+
#
|
81
|
+
# @see Flows::Flow some examples here
|
82
|
+
class Node
|
83
|
+
# Node metadata, a frozen Ruby Hash.
|
84
|
+
attr_reader :meta
|
85
|
+
attr_reader :router
|
86
|
+
|
87
|
+
# @param body [Proc] node body
|
88
|
+
# @param router [Router] node router
|
89
|
+
# @param meta [Hash] node metadata
|
90
|
+
# @param preprocessor [Proc, nil] pre-processor for the node's body
|
91
|
+
# @param postprocessor [Proc, nil] post-processor for the node's body
|
92
|
+
def initialize(body:, router:, meta: {}, preprocessor: nil, postprocessor: nil)
|
93
|
+
@body = body
|
94
|
+
@router = router
|
95
|
+
|
96
|
+
@meta = meta.freeze
|
97
|
+
|
98
|
+
@preprocessor = preprocessor
|
99
|
+
@postprocessor = postprocessor
|
100
|
+
end
|
101
|
+
|
102
|
+
# Executes the node.
|
103
|
+
#
|
104
|
+
# @param input [Object] input for a node. In the context of {Flow}
|
105
|
+
# it's initial input or output of the previously executed node.
|
106
|
+
#
|
107
|
+
# @param context [Hash] execution context. In case of {Flow}
|
108
|
+
# shared between node executions.
|
109
|
+
#
|
110
|
+
# @return [Array<(Object, Symbol)>] output of a node and next route.
|
111
|
+
#
|
112
|
+
# `:reek:TooManyStatements` is disabled for this method because even
|
113
|
+
# one more call to a private method impacts performance here.
|
114
|
+
def call(input, context:)
|
115
|
+
output =
|
116
|
+
if @preprocessor
|
117
|
+
args, kwargs = @preprocessor.call(input, context, @meta)
|
118
|
+
|
119
|
+
# https://bugs.ruby-lang.org/issues/14415
|
120
|
+
kwargs.empty? ? @body.call(*args) : @body.call(*args, **kwargs)
|
121
|
+
else
|
122
|
+
@body.call(input)
|
123
|
+
end
|
124
|
+
output = @postprocessor.call(output, context, @meta) if @postprocessor
|
125
|
+
|
126
|
+
route = @router.call(output)
|
127
|
+
|
128
|
+
[output, route]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Flows
|
2
|
+
class Flow
|
3
|
+
# @abstract
|
4
|
+
#
|
5
|
+
# Node router: defines rules to calculate next {Node} to execute inside a particular {Flow}.
|
6
|
+
#
|
7
|
+
# Router receives {Flows::Result} Object, execution context and execution metadata.
|
8
|
+
# Basing on this information a router must decide what to execute next or
|
9
|
+
# decide to stop execution of a flow.
|
10
|
+
#
|
11
|
+
# If router returns `:end` - it stops an execution process.
|
12
|
+
#
|
13
|
+
# @!method call( result )
|
14
|
+
# @abstract
|
15
|
+
# @param result [Flows::Result] Result Object, output of a {Node} execution.
|
16
|
+
# @return [Symbol] name of the next node or a special symbol `:end`.
|
17
|
+
# @raise [NoRouteError] if cannot determine a route.
|
18
|
+
#
|
19
|
+
# @!method destinations
|
20
|
+
# @abstract
|
21
|
+
# @return [Array<Symbol>] names of all the possible destination nodes
|
22
|
+
class Router
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require_relative 'router/errors'
|
28
|
+
require_relative 'router/simple'
|
29
|
+
require_relative 'router/custom'
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Flows
|
2
|
+
class Flow
|
3
|
+
class Router
|
4
|
+
# Router with custom rules.
|
5
|
+
#
|
6
|
+
# Expects routes table in a special format:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# ->(x) { x.ok? } => :step_a,
|
10
|
+
# ->(x) { x.err? && x.status == :validation_error } => :end,
|
11
|
+
# ->(x) { x.err? } => :handle_error
|
12
|
+
# }
|
13
|
+
#
|
14
|
+
# Yes, it's confusing. But by including {Flows::Result::Helpers} you can (and should) rewrite it like this:
|
15
|
+
#
|
16
|
+
# {
|
17
|
+
# match_ok => :route_a, # on success go to step_a
|
18
|
+
# match_err(:validation_error) => :end, # on failure with status `:validation_error` - stop execution
|
19
|
+
# match_err => :handle_error # on any other failure go to the step handle_error
|
20
|
+
# }
|
21
|
+
#
|
22
|
+
# So, your routes table is an ordered set of pairs `predicate => route` in form of Ruby Hash.
|
23
|
+
#
|
24
|
+
# Any time you writing a router table you can imagine that you're writing `case`:
|
25
|
+
#
|
26
|
+
# case step_result
|
27
|
+
# when match_ok then :route_a # on success go to step_a
|
28
|
+
# when match_err(:validation_error) then :end # on failure with status `:validation_error` - stop execution
|
29
|
+
# when match_err then :handle_error # on any other failure go to the step handle_error
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @see Flows::Flow some examples here
|
33
|
+
#
|
34
|
+
# @see Flows::Flow::Node Pre/postprocessing of data must be done inside Node.
|
35
|
+
class Custom < Router
|
36
|
+
# Creates a new custom router from a route table.
|
37
|
+
#
|
38
|
+
# @param routes [Hash<Proc, Symbol>] route table.
|
39
|
+
def initialize(routes)
|
40
|
+
@routes = routes
|
41
|
+
end
|
42
|
+
|
43
|
+
# @see Flows::Flow::Router#call
|
44
|
+
def call(result)
|
45
|
+
@routes.each_pair do |predicate, route|
|
46
|
+
return route if predicate === result # rubocop:disable Style/CaseEquality
|
47
|
+
end
|
48
|
+
|
49
|
+
raise NoRouteError, "no route found for: `#{result.inspect}`"
|
50
|
+
end
|
51
|
+
|
52
|
+
# @see Flows::Flow::Router#destinations
|
53
|
+
def destinations
|
54
|
+
@routes.values
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|