flows 0.3.0 → 0.4.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +1 -0
  4. data/.reek.yml +42 -0
  5. data/.rubocop.yml +20 -7
  6. data/.ruby-version +1 -1
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +0 -6
  10. data/Gemfile.lock +139 -74
  11. data/README.md +158 -364
  12. data/Rakefile +35 -1
  13. data/bin/.rubocop.yml +5 -0
  14. data/bin/all_the_errors +47 -0
  15. data/bin/benchmark +73 -105
  16. data/bin/benchmark_cli/compare.rb +118 -0
  17. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  18. data/bin/benchmark_cli/compare/base.rb +45 -0
  19. data/bin/benchmark_cli/compare/command.rb +47 -0
  20. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  21. data/bin/benchmark_cli/examples.rb +23 -0
  22. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  38. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  39. data/bin/benchmark_cli/helpers.rb +12 -0
  40. data/bin/benchmark_cli/ruby.rb +15 -0
  41. data/bin/benchmark_cli/ruby/command.rb +38 -0
  42. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  43. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  44. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  45. data/bin/console +1 -0
  46. data/bin/docserver +7 -0
  47. data/bin/errors +118 -0
  48. data/bin/errors_cli/contract_error_demo.rb +49 -0
  49. data/bin/errors_cli/di_error_demo.rb +38 -0
  50. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  51. data/bin/errors_cli/oc_error_demo.rb +40 -0
  52. data/bin/errors_cli/railway_error_demo.rb +10 -0
  53. data/bin/errors_cli/result_error_demo.rb +13 -0
  54. data/bin/errors_cli/scp_error_demo.rb +17 -0
  55. data/docs/README.md +2 -186
  56. data/docs/_sidebar.md +0 -24
  57. data/docs/index.html +1 -1
  58. data/flows.gemspec +25 -2
  59. data/forspell.dict +9 -0
  60. data/lefthook.yml +9 -0
  61. data/lib/flows.rb +11 -5
  62. data/lib/flows/contract.rb +402 -0
  63. data/lib/flows/contract/array.rb +55 -0
  64. data/lib/flows/contract/case_eq.rb +41 -0
  65. data/lib/flows/contract/compose.rb +77 -0
  66. data/lib/flows/contract/either.rb +53 -0
  67. data/lib/flows/contract/error.rb +25 -0
  68. data/lib/flows/contract/hash.rb +75 -0
  69. data/lib/flows/contract/hash_of.rb +70 -0
  70. data/lib/flows/contract/helpers.rb +22 -0
  71. data/lib/flows/contract/predicate.rb +34 -0
  72. data/lib/flows/contract/transformer.rb +50 -0
  73. data/lib/flows/contract/tuple.rb +70 -0
  74. data/lib/flows/flow.rb +75 -7
  75. data/lib/flows/flow/node.rb +131 -0
  76. data/lib/flows/flow/router.rb +25 -0
  77. data/lib/flows/flow/router/custom.rb +54 -0
  78. data/lib/flows/flow/router/errors.rb +11 -0
  79. data/lib/flows/flow/router/simple.rb +20 -0
  80. data/lib/flows/plugin.rb +13 -0
  81. data/lib/flows/plugin/dependency_injector.rb +159 -0
  82. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  83. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  84. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  85. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  86. data/lib/flows/plugin/implicit_init.rb +45 -0
  87. data/lib/flows/plugin/output_contract.rb +84 -0
  88. data/lib/flows/plugin/output_contract/dsl.rb +36 -0
  89. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  90. data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
  91. data/lib/flows/railway.rb +140 -37
  92. data/lib/flows/railway/dsl.rb +8 -19
  93. data/lib/flows/railway/errors.rb +8 -12
  94. data/lib/flows/railway/step.rb +24 -0
  95. data/lib/flows/railway/step_list.rb +38 -0
  96. data/lib/flows/result.rb +188 -2
  97. data/lib/flows/result/do.rb +160 -16
  98. data/lib/flows/result/err.rb +12 -6
  99. data/lib/flows/result/errors.rb +29 -17
  100. data/lib/flows/result/helpers.rb +25 -3
  101. data/lib/flows/result/ok.rb +12 -6
  102. data/lib/flows/shared_context_pipeline.rb +216 -0
  103. data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
  104. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  105. data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
  106. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  107. data/lib/flows/shared_context_pipeline/step.rb +46 -0
  108. data/lib/flows/shared_context_pipeline/track.rb +67 -0
  109. data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
  110. data/lib/flows/util.rb +17 -0
  111. data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
  112. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
  113. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
  114. data/lib/flows/util/prepend_to_class.rb +145 -0
  115. data/lib/flows/version.rb +1 -1
  116. metadata +233 -37
  117. data/bin/demo +0 -66
  118. data/bin/examples.rb +0 -195
  119. data/bin/profile_10steps +0 -106
  120. data/bin/ruby_benchmarks +0 -26
  121. data/docs/CNAME +0 -1
  122. data/docs/contributing/benchmarks_profiling.md +0 -3
  123. data/docs/contributing/local_development.md +0 -3
  124. data/docs/flow/direct_usage.md +0 -3
  125. data/docs/flow/general_idea.md +0 -3
  126. data/docs/operation/basic_usage.md +0 -1
  127. data/docs/operation/inject_steps.md +0 -3
  128. data/docs/operation/lambda_steps.md +0 -3
  129. data/docs/operation/result_shapes.md +0 -3
  130. data/docs/operation/routing_tracks.md +0 -3
  131. data/docs/operation/wrapping_steps.md +0 -3
  132. data/docs/overview/performance.md +0 -336
  133. data/docs/railway/basic_usage.md +0 -232
  134. data/docs/result_objects/basic_usage.md +0 -196
  135. data/docs/result_objects/do_notation.md +0 -139
  136. data/lib/flows/implicit_build.rb +0 -16
  137. data/lib/flows/node.rb +0 -27
  138. data/lib/flows/operation.rb +0 -55
  139. data/lib/flows/operation/builder.rb +0 -130
  140. data/lib/flows/operation/builder/build_router.rb +0 -37
  141. data/lib/flows/operation/dsl.rb +0 -93
  142. data/lib/flows/operation/errors.rb +0 -75
  143. data/lib/flows/operation/executor.rb +0 -78
  144. data/lib/flows/railway/builder.rb +0 -68
  145. data/lib/flows/railway/executor.rb +0 -23
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
@@ -0,0 +1,50 @@
1
+ module Flows
2
+ class Contract
3
+ # Adds transformation to an existing contract.
4
+ #
5
+ # If original contract already has a transform -
6
+ # final transformation will be composition of original and new one.
7
+ #
8
+ # You MUST obey Transformation Laws (see {Contract} documentation for details).
9
+ #
10
+ # @example Upcase strings
11
+ # up_str = Flows::Contract::Transformer.new(String) { |str| str.upcase }
12
+ #
13
+ # up_str.transform!('megatron')
14
+ # # => 'MEGATRON'
15
+ #
16
+ # up_str.transform(:megatron).error
17
+ # # => 'must match `String`'
18
+ #
19
+ # @example Strip and upcase strings
20
+ # strip_str = Flows::Contract::Transformer.new(String, &:strip)
21
+ # up_stip_str = Flows::Contract::Transformer.new(strip_str, &:upcase)
22
+ #
23
+ # up_str.transform!(' megatron ')
24
+ # # => 'MEGATRON'
25
+ #
26
+ # up_str.cast(:megatron).error
27
+ # # => 'must match `String`'
28
+ class Transformer < Contract
29
+ # @param contract [Contract, Object] in case of non-contract argument {CaseEq} is automatically applied.
30
+ # @yield [object] transform implementation
31
+ # @yieldreturn [object] result of transform. Must obey transformation laws.
32
+ def initialize(contract, &transform_proc)
33
+ @contract = to_contract(contract)
34
+ @transform = transform_proc
35
+ end
36
+
37
+ # @see Contract#check!
38
+ def check!(other)
39
+ @contract.check!(other)
40
+ end
41
+
42
+ # @see Contract#transform!
43
+ def transform!(other)
44
+ @transform.call(
45
+ @contract.transform!(other)
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
@@ -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,18 +1,86 @@
1
+ require_relative 'flow/node'
2
+ require_relative 'flow/router'
3
+
1
4
  module Flows
2
- # Simple sequential flow
5
+ # Abstraction for build [deterministic finite-state machine](https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/)-like
6
+ # execution objects.
7
+ #
8
+ # Let's refer to 'deterministic finite-state machine' as DFSM.
9
+ #
10
+ # It's NOT an implementation of DFSM. It just shares a lot of
11
+ # structural ideas. You can also think about {Flow} as an [oriented graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)#Oriented_graph),
12
+ # where:
13
+ #
14
+ # * you have the one and only one initial node
15
+ # * you start execution from the initial node
16
+ # * after node execution your are going to some next node or stop execution.
17
+ #
18
+ # And edges formed by possible next nodes.
19
+ #
20
+ # DFSM has a very important property:
21
+ #
22
+ # > From any state, there is only one transition for any allowed input.
23
+ #
24
+ # So, we represent DFSM states as {Node}s. Each {Node}, basing on input (input includes execution context also)
25
+ # performs some side effects and returns output and next {Node} name (DFSM state).
26
+ #
27
+ # Side effects here can be spitted into two types:
28
+ #
29
+ # * modification of execution context
30
+ # * rest of them: working with 3rd party libraries, printing to STDOUT, etc.
31
+ #
32
+ # Final state represented by special symbol `:end`.
33
+ #
34
+ # @note You should not use {Flow} in your business code. It's designed to be underlying execution engine
35
+ # for high-level abstractions. In other words - it's for libraries, not applications.
36
+ #
37
+ # @example Calculates sum of elements in array. If sum more than 10 prints 'Big', otherwise prints 'Small'.
38
+ #
39
+ # flow = Flows::Flow.new(
40
+ # start_node: :sum_list,
41
+ # node_map: {
42
+ # sum_list: Flows::Flow::Node.new(
43
+ # body: ->(list) { list.sum },
44
+ # router: Flows::Flow::Router::Custom.new(
45
+ # ->(x) { x > 10 } => :print_big,
46
+ # ->(x) { x <= 10 } => :print_small
47
+ # )
48
+ # ),
49
+ # print_big: Flows::Flow::Node.new(
50
+ # body: ->(_) { puts 'Big' },
51
+ # router: Flows::Flow::Router::Custom.new(
52
+ # nil => :end # puts returns nil.
53
+ # )
54
+ # ),
55
+ # print_small: Flows::Flow::Node.new(
56
+ # body: ->(_) { puts 'Small' },
57
+ # router: Flows::Flow::Router::Custom.new(
58
+ # nil => :end # puts returns nil.
59
+ # )
60
+ # )
61
+ # }
62
+ # )
63
+ #
64
+ # flow.call([1, 2, 3, 4, 5], context: {})
65
+ # # prints 'Big' and returns nil
3
66
  class Flow
4
- def initialize(start_node:, nodes:)
67
+ # @param start_node [Symbol] name of the entry node.
68
+ # @param node_map [Hash<Symbol, Node>] keys are node names, values are nodes.
69
+ def initialize(start_node:, node_map:)
5
70
  @start_node = start_node
6
- @nodes = Hash[
7
- nodes.map { |node| [node.name, node] }
8
- ]
71
+ @node_map = node_map
9
72
  end
10
73
 
74
+ # Executes a flow.
75
+ #
76
+ # @param input [Object] initial input
77
+ # @param context [Hash] mutable execution context
78
+ # @return [Object] execution result
11
79
  def call(input, context:)
12
80
  current_node_name = @start_node
13
81
 
14
- while current_node_name != :term
15
- input, current_node_name = @nodes[current_node_name].call(input, context: context)
82
+ while current_node_name != :end
83
+ input, current_node_name = @node_map[current_node_name].call(input, context: context)
16
84
  end
17
85
 
18
86
  input
@@ -0,0 +1,131 @@
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
+
86
+ # @param body [Proc] node body
87
+ # @param router [Router] node router
88
+ # @param meta [Hash] node metadata
89
+ # @param preprocessor [Proc, nil] pre-processor for the node's body
90
+ # @param postprocessor [Proc, nil] post-processor for the node's body
91
+ def initialize(body:, router:, meta: {}, preprocessor: nil, postprocessor: nil)
92
+ @body = body
93
+ @router = router
94
+
95
+ @meta = meta.freeze
96
+
97
+ @preprocessor = preprocessor
98
+ @postprocessor = postprocessor
99
+ end
100
+
101
+ # Executes the node.
102
+ #
103
+ # @param input [Object] input for a node. In the context of {Flow}
104
+ # it's initial input or output of the previously executed node.
105
+ #
106
+ # @param context [Hash] execution context. In case of {Flow}
107
+ # shared between node executions.
108
+ #
109
+ # @return [Array<(Object, Symbol)>] output of a node and next route.
110
+ #
111
+ # `:reek:TooManyStatements` is disabled for this method because even
112
+ # one more call to a private method impacts performance here.
113
+ def call(input, context:)
114
+ output =
115
+ if @preprocessor
116
+ args, kwargs = @preprocessor.call(input, context, @meta)
117
+
118
+ # https://bugs.ruby-lang.org/issues/14415
119
+ kwargs.empty? ? @body.call(*args) : @body.call(*args, **kwargs)
120
+ else
121
+ @body.call(input)
122
+ end
123
+ output = @postprocessor.call(output, context, @meta) if @postprocessor
124
+
125
+ route = @router.call(output)
126
+
127
+ [output, route]
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,25 @@
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
+ class Router
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative 'router/errors'
24
+ require_relative 'router/simple'
25
+ require_relative 'router/custom'
@@ -0,0 +1,54 @@
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
+ end
52
+ end
53
+ end
54
+ 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
@@ -0,0 +1,20 @@
1
+ module Flows
2
+ class Flow
3
+ class Router
4
+ # Router with static paths for successful and failure results.
5
+ class Simple < Router
6
+ # @param success_route [Symbol] route for any successful results.
7
+ # @param failure_route [Symbol] route for any failure results.
8
+ def initialize(success_route, failure_route)
9
+ @success_route = success_route
10
+ @failure_route = failure_route
11
+ end
12
+
13
+ # @see Flows::Flow::Router#call
14
+ def call(result)
15
+ result.ok? ? @success_route : @failure_route
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end