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.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -1
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +26 -7
  7. data/.rubocop_todo.yml +27 -0
  8. data/.ruby-version +1 -1
  9. data/.yardopts +1 -0
  10. data/CHANGELOG.md +81 -0
  11. data/Gemfile +0 -6
  12. data/README.md +167 -363
  13. data/Rakefile +35 -1
  14. data/bin/.rubocop.yml +5 -0
  15. data/bin/all_the_errors +55 -0
  16. data/bin/benchmark +73 -105
  17. data/bin/benchmark_cli/compare.rb +118 -0
  18. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  19. data/bin/benchmark_cli/compare/base.rb +45 -0
  20. data/bin/benchmark_cli/compare/command.rb +47 -0
  21. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  22. data/bin/benchmark_cli/examples.rb +23 -0
  23. data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  31. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  33. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  38. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  39. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  40. data/bin/benchmark_cli/helpers.rb +12 -0
  41. data/bin/benchmark_cli/ruby.rb +15 -0
  42. data/bin/benchmark_cli/ruby/command.rb +38 -0
  43. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  44. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  45. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  46. data/bin/console +1 -0
  47. data/bin/docserver +7 -0
  48. data/bin/errors +138 -0
  49. data/bin/errors_cli/contract_error_demo.rb +49 -0
  50. data/bin/errors_cli/di_error_demo.rb +38 -0
  51. data/bin/errors_cli/flow_error_demo.rb +22 -0
  52. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  53. data/bin/errors_cli/interface_error_demo.rb +17 -0
  54. data/bin/errors_cli/oc_error_demo.rb +40 -0
  55. data/bin/errors_cli/railway_error_demo.rb +10 -0
  56. data/bin/errors_cli/result_error_demo.rb +13 -0
  57. data/bin/errors_cli/scp_error_demo.rb +17 -0
  58. data/docs/README.md +3 -187
  59. data/docs/_sidebar.md +0 -24
  60. data/docs/index.html +1 -1
  61. data/flows.gemspec +27 -2
  62. data/forspell.dict +9 -0
  63. data/lefthook.yml +9 -0
  64. data/lib/flows.rb +11 -5
  65. data/lib/flows/contract.rb +402 -0
  66. data/lib/flows/contract/array.rb +55 -0
  67. data/lib/flows/contract/case_eq.rb +43 -0
  68. data/lib/flows/contract/compose.rb +77 -0
  69. data/lib/flows/contract/either.rb +53 -0
  70. data/lib/flows/contract/error.rb +24 -0
  71. data/lib/flows/contract/hash.rb +75 -0
  72. data/lib/flows/contract/hash_of.rb +70 -0
  73. data/lib/flows/contract/helpers.rb +22 -0
  74. data/lib/flows/contract/predicate.rb +34 -0
  75. data/lib/flows/contract/transformer.rb +50 -0
  76. data/lib/flows/contract/tuple.rb +70 -0
  77. data/lib/flows/flow.rb +96 -7
  78. data/lib/flows/flow/errors.rb +29 -0
  79. data/lib/flows/flow/node.rb +132 -0
  80. data/lib/flows/flow/router.rb +29 -0
  81. data/lib/flows/flow/router/custom.rb +59 -0
  82. data/lib/flows/flow/router/errors.rb +11 -0
  83. data/lib/flows/flow/router/simple.rb +25 -0
  84. data/lib/flows/plugin.rb +15 -0
  85. data/lib/flows/plugin/dependency_injector.rb +170 -0
  86. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  87. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  88. data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
  89. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  90. data/lib/flows/plugin/implicit_init.rb +45 -0
  91. data/lib/flows/plugin/interface.rb +84 -0
  92. data/lib/flows/plugin/output_contract.rb +85 -0
  93. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  94. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  95. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  96. data/lib/flows/plugin/profiler.rb +114 -0
  97. data/lib/flows/plugin/profiler/injector.rb +35 -0
  98. data/lib/flows/plugin/profiler/report.rb +48 -0
  99. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  100. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  101. data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
  102. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  103. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  104. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  105. data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
  106. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  107. data/lib/flows/railway.rb +140 -34
  108. data/lib/flows/railway/dsl.rb +8 -18
  109. data/lib/flows/railway/errors.rb +8 -12
  110. data/lib/flows/railway/step.rb +24 -0
  111. data/lib/flows/railway/step_list.rb +38 -0
  112. data/lib/flows/result.rb +188 -2
  113. data/lib/flows/result/do.rb +158 -16
  114. data/lib/flows/result/err.rb +12 -6
  115. data/lib/flows/result/errors.rb +29 -17
  116. data/lib/flows/result/helpers.rb +25 -3
  117. data/lib/flows/result/ok.rb +12 -6
  118. data/lib/flows/shared_context_pipeline.rb +342 -0
  119. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  120. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
  121. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  122. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  123. data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
  124. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  125. data/lib/flows/shared_context_pipeline/step.rb +55 -0
  126. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  127. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  128. data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
  129. data/lib/flows/util.rb +17 -0
  130. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  131. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
  132. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  133. data/lib/flows/util/prepend_to_class.rb +191 -0
  134. data/lib/flows/version.rb +1 -1
  135. metadata +253 -38
  136. data/Gemfile.lock +0 -174
  137. data/bin/demo +0 -66
  138. data/bin/examples.rb +0 -195
  139. data/bin/profile_10steps +0 -106
  140. data/bin/ruby_benchmarks +0 -26
  141. data/docs/CNAME +0 -1
  142. data/docs/contributing/benchmarks_profiling.md +0 -3
  143. data/docs/contributing/local_development.md +0 -3
  144. data/docs/flow/direct_usage.md +0 -3
  145. data/docs/flow/general_idea.md +0 -3
  146. data/docs/operation/basic_usage.md +0 -1
  147. data/docs/operation/inject_steps.md +0 -3
  148. data/docs/operation/lambda_steps.md +0 -3
  149. data/docs/operation/result_shapes.md +0 -3
  150. data/docs/operation/routing_tracks.md +0 -3
  151. data/docs/operation/wrapping_steps.md +0 -3
  152. data/docs/overview/performance.md +0 -336
  153. data/docs/railway/basic_usage.md +0 -232
  154. data/docs/result_objects/basic_usage.md +0 -196
  155. data/docs/result_objects/do_notation.md +0 -139
  156. data/lib/flows/node.rb +0 -27
  157. data/lib/flows/operation.rb +0 -52
  158. data/lib/flows/operation/builder.rb +0 -130
  159. data/lib/flows/operation/builder/build_router.rb +0 -37
  160. data/lib/flows/operation/dsl.rb +0 -93
  161. data/lib/flows/operation/errors.rb +0 -75
  162. data/lib/flows/operation/executor.rb +0 -78
  163. data/lib/flows/railway/builder.rb +0 -68
  164. data/lib/flows/railway/executor.rb +0 -23
  165. data/lib/flows/result_router.rb +0 -14
  166. 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
@@ -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
- # Simple sequential flow
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
- def initialize(start_node:, nodes:)
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
- @nodes = Hash[
7
- nodes.map { |node| [node.name, node] }
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 != :term
15
- input, current_node_name = @nodes[current_node_name].call(input, context: context)
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
@@ -0,0 +1,11 @@
1
+ module Flows
2
+ class Flow
3
+ class Router
4
+ # Base class for {Flows::Router} error.
5
+ class Error < Flows::Error; end
6
+
7
+ # Raised when no route found basing on provided data.
8
+ class NoRouteError < Error; end
9
+ end
10
+ end
11
+ end