flows 0.2.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|