flows 0.2.0 → 0.6.0

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