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
@@ -1,29 +1,171 @@
1
1
  module Flows
2
2
  class Result
3
- # Do-notation for Result Objects
3
+ # Do-notation for Result Objects.
4
+ #
5
+ # This functionality aims to simplify common control flow pattern:
6
+ # when you have to stop execution on a first failure and return this failure.
7
+ # Do Notation inspired by [Do Notation in dry-rb](https://dry-rb.org/gems/dry-monads/1.3/do-notation/)
8
+ # and [Haskell do keyword](https://wiki.haskell.org/Keywords#do).
9
+ #
10
+ # Sometimes you have to write something like this:
11
+ #
12
+ # class Something
13
+ # include Flows::Result::Helpers
14
+ #
15
+ # def perform
16
+ # user_result = fetch_user
17
+ # return user_result if user_result.err?
18
+ #
19
+ # data_result = fetch_data
20
+ # return data_result if data_result.err?
21
+ #
22
+ # calculation_result = calculation(user_result.unwrap[:user], data_result.unwrap)
23
+ # return calculation_result if user_result.err?
24
+ #
25
+ # ok(data: calculation_result.unwrap[:some_field])
26
+ # end
27
+ #
28
+ # private
29
+ #
30
+ # def fetch_user
31
+ # # returns Ok or Err
32
+ # end
33
+ #
34
+ # def fetch_data
35
+ # # returns Ok or Err
36
+ # end
37
+ #
38
+ # def calculation(_user, _data)
39
+ # # returns Ok or Err
40
+ # end
41
+ # end
42
+ #
43
+ # The main idea of the code above is to stop method execution and
44
+ # return failed Result Object if one of the sub-operations is failed.
45
+ # At the moment of failure.
46
+ #
47
+ # By using Do Notation feature you may rewrite it like this:
48
+ #
49
+ # class SomethingWithDoNotation
50
+ # include Flows::Result::Helpers
51
+ # extend Flows::Result::Do # enable Do Notation
52
+ #
53
+ # do_notation(:perform) # changes behaviour of `yield` in this method
54
+ # def perform
55
+ # user = yield(fetch_user)[:user] # yield here returns array of one element
56
+ # data = yield fetch_data # yield here returns a Hash
57
+ #
58
+ # ok(
59
+ # data: yield(calculation(user, data))[:some_field]
60
+ # )
61
+ # end
62
+ #
63
+ # # private method definitions
64
+ # end
65
+ #
66
+ # `do_notation(:perform)` makes some wrapping here and allows you to use `yield`
67
+ # inside the `perform` method in a non standard way:
68
+ # to unpack results or instantly leave a method if a failed result was provided.
69
+ #
70
+ # ## How to use it
71
+ #
72
+ # First of all, you have to include `Flows::Result::Do` mixin into your class or module.
73
+ # It adds `do_notation` class method.
74
+ # `do_notation` accepts method name as an argument and changes behaviour of `yield` inside this method.
75
+ # By the way, when you are using `do_notation` you cannot pass a block to modified method anymore.
76
+ #
77
+ # class MyClass
78
+ # extend Flows::Result::Do
79
+ #
80
+ # do_notation(:my_method_1)
81
+ # def my_method_1
82
+ # # some code
83
+ # end
84
+ #
85
+ # do_notation(:my_method_2)
86
+ # def my_method_2
87
+ # # some code
88
+ # end
89
+ # end
90
+ #
91
+ # `yield` in such methods is working by the following rules:
92
+ #
93
+ # ok_result = Flows::Result::Ok.new(a: 1, b: 2)
94
+ # err_result = Flows::Result::Err.new(x: 1, y: 2)
95
+ #
96
+ # # the following three lines are equivalent
97
+ # yield(ok_result)
98
+ # ok_result.unwrap
99
+ # { a: 1, b: 2 }
100
+ #
101
+ # # the following three lines are equivalent
102
+ # yield(:a, :b, ok_result)
103
+ # ok_result.unwrap.values_at(:a, :b)
104
+ # [1, 2]
105
+ #
106
+ # # the following three lines are equivalent
107
+ # return err_result
108
+ # yield(err_result)
109
+ # yield(:x, :y, err_result)
110
+ #
111
+ # As you may see, `yield` has two forms of usage:
112
+ #
113
+ # * `yield(result_value)` - returns unwrapped data Hash for successful results or,
114
+ # in case of failed result, stops method execution and returns failed `result_value` as a method result.
115
+ # * `yield(*keys, result_value)` - returns unwrapped data under provided keys as Array for successful results or,
116
+ # in case of failed result, stops method execution and returns failed `result_value` as a method result.
117
+ #
118
+ # ## How it works
119
+ #
120
+ # Under the hood `Flows::Result::Do` creates a module and prepends it to your class or module.
121
+ # Invoking `do_notation(:method_name)` adds special wrapper method to the prepended module.
122
+ # So, when you perform call to `YourClassOrModule#method_name` - you're executing wrapper in
123
+ # the prepended module.
4
124
  module Do
5
- # DSL for Do-notation
6
- module DSL
7
- def do_for(method_name)
8
- @flows_result_do_module.define_method(method_name) do |*args|
9
- super(*args) do |*fields, result|
10
- case result
11
- when Flows::Result::Ok
12
- fields.any? ? result.unwrap.values_at(*fields) : result.unwrap
13
- when Flows::Result::Err then return result
14
- else raise "Unexpected result: #{result.inspect}. Should be a Flows::Result"
125
+ MOD_VAR_NAME = :@flows_result_module_for_do
126
+
127
+ SingletonVarsSetup = ::Flows::Util::InheritableSingletonVars::IsolationStrategy.make_module(
128
+ MOD_VAR_NAME => -> { Module.new }
129
+ )
130
+
131
+ include SingletonVarsSetup
132
+
133
+ # Utility functions for Flows::Result::Do.
134
+ #
135
+ # Isolated location prevents polluting user classes with unnecessary methods.
136
+ #
137
+ # @api private
138
+ module Util
139
+ class << self
140
+ def fetch_and_prepend_module(mod)
141
+ module_for_do = mod.instance_variable_get(MOD_VAR_NAME)
142
+ mod.prepend(module_for_do)
143
+ module_for_do
144
+ end
145
+
146
+ # `:reek:TooManyStatements` - allowed because we have no choice here.
147
+ #
148
+ # `:reek:NestedIterators` - allowed here because here are no iterators.
149
+ def define_wrapper(mod, method_name) # rubocop:disable Metrics/MethodLength
150
+ mod.define_method(method_name) do |*args|
151
+ super(*args) do |*fields, result|
152
+ case result
153
+ when Flows::Result::Ok
154
+ data = result.unwrap
155
+ fields.any? ? data.values_at(*fields) : data
156
+ when Flows::Result::Err then return result
157
+ else raise "Unexpected result: #{result.inspect}. Should be a Flows::Result"
158
+ end
15
159
  end
16
160
  end
17
161
  end
18
162
  end
19
163
  end
20
164
 
21
- def self.included(mod)
22
- patch_mod = Module.new
165
+ def do_notation(method_name)
166
+ prepended_mod = Util.fetch_and_prepend_module(self)
23
167
 
24
- mod.instance_variable_set(:@flows_result_do_module, patch_mod)
25
- mod.prepend(patch_mod)
26
- mod.extend(DSL)
168
+ Util.define_wrapper(prepended_mod, method_name)
27
169
  end
28
170
  end
29
171
  end
@@ -1,25 +1,31 @@
1
1
  module Flows
2
2
  class Result
3
- # Wrapper for failure results
3
+ # Result Object for failure results.
4
+ #
5
+ # @see Flows::Result behaviour described here
4
6
  class Err < Result
5
- attr_reader :error
6
-
7
- def initialize(data, status: :failure, meta: {})
8
- @error = data
7
+ def initialize(data, status: :err, meta: {})
8
+ @data = data
9
9
  @status = status
10
10
  @meta = meta
11
11
  end
12
12
 
13
+ def error
14
+ @data
15
+ end
16
+
17
+ # @return [false]
13
18
  def ok?
14
19
  false
15
20
  end
16
21
 
22
+ # @return [true]
17
23
  def err?
18
24
  true
19
25
  end
20
26
 
21
27
  def unwrap
22
- raise UnwrapError.new(@status, @data, @meta)
28
+ raise AccessError, self
23
29
  end
24
30
  end
25
31
  end
@@ -1,29 +1,41 @@
1
1
  module Flows
2
2
  class Result
3
- # Error for unwrapping non-successful result object
4
- class UnwrapError < Flows::Error
5
- def initialize(status, data, meta)
6
- @status = status
7
- @data = data
8
- @meta = meta
3
+ # Base class for Result errors.
4
+ class Error < ::Flows::Error; end
5
+
6
+ # Error for invalid data access cases
7
+ class AccessError < Flows::Error
8
+ def initialize(result)
9
+ @result = result
9
10
  end
10
11
 
11
12
  def message
12
- "You're trying to unwrap non-successful result with status `#{@status.inspect}` and data `#{@data.inspect}`\n\
13
- Result metadata: `#{@meta.inspect}`"
13
+ [
14
+ base_msg,
15
+ " Result status: `#{@result.status.inspect}`",
16
+ " Result data: `#{data.inspect}`",
17
+ " Result meta: `#{@result.meta.inspect}`"
18
+ ].join("\n")
14
19
  end
15
- end
16
20
 
17
- # Error for dealing with failure result as success one
18
- class NoErrorError < Flows::Error
19
- def initialize(status, data)
20
- @status = status
21
- @data = data
21
+ private
22
+
23
+ def base_msg
24
+ case @result
25
+ when Flows::Result::Ok
26
+ 'Data in a successful result must be retrieved using `#unwrap` method, not `#error`.'
27
+ when Flows::Result::Err
28
+ 'Data in a failure result must be retrieved using `#error` method, not `#unwrap`.'
29
+ end
22
30
  end
23
31
 
24
- def message
25
- "You're trying to get error data for successful result with status \
26
- `#{@status.inspect}` and data `#{@data.inspect}`"
32
+ def data
33
+ case @result
34
+ when Flows::Result::Ok
35
+ @result.unwrap
36
+ when Flows::Result::Err
37
+ @result.error
38
+ end
27
39
  end
28
40
  end
29
41
  end
@@ -1,14 +1,36 @@
1
1
  module Flows
2
2
  class Result
3
- # Shortcuts for building result objects
3
+ # Shortcuts for building and matching result objects.
4
+ #
5
+ # `:reek:UtilityFunction` and `:reek:FeatureEnvy` checks should be disabled here
6
+ # because this module is intended to contain private utility methods only.
7
+ #
8
+ # This module defines the following private methods:
9
+ #
10
+ # * `ok(status = :ok, **data)` - for building successful results from a hash of keyword arguments.
11
+ # * `ok_data(data, status: :ok)` - for building successful results from any data.
12
+ # * `err(status = :err, **data)` - for building failure results from a hash of keyword arguments.
13
+ # * `err_data(data, status: :err)` - for building failure results from any data.
14
+ # * `match_ok(status = nil)` - for case matching against successful results.
15
+ # * `match_err(status = nil)` - for case matching against failure results.
16
+ #
17
+ # @see Flows::Result usage examples provided here
4
18
  module Helpers
5
19
  private
6
20
 
7
- def ok(status = :success, **data)
21
+ def ok(status = :ok, **data)
8
22
  Flows::Result::Ok.new(data, status: status)
9
23
  end
10
24
 
11
- def err(status = :failure, **data)
25
+ def ok_data(data, status: :ok)
26
+ Flows::Result::Ok.new(data, status: status)
27
+ end
28
+
29
+ def err(status = :err, **data)
30
+ Flows::Result::Err.new(data, status: status)
31
+ end
32
+
33
+ def err_data(data, status: :err)
12
34
  Flows::Result::Err.new(data, status: status)
13
35
  end
14
36
 
@@ -1,25 +1,31 @@
1
1
  module Flows
2
2
  class Result
3
- # Wrapper for successful results
3
+ # Result Object for successful results.
4
+ #
5
+ # @see Flows::Result behaviour described here
4
6
  class Ok < Result
5
- attr_reader :unwrap
6
-
7
- def initialize(data, status: :success, meta: {})
8
- @unwrap = data
7
+ def initialize(data, status: :ok, meta: {})
8
+ @data = data
9
9
  @status = status
10
10
  @meta = meta
11
11
  end
12
12
 
13
+ def unwrap
14
+ @data
15
+ end
16
+
17
+ # @return [true]
13
18
  def ok?
14
19
  true
15
20
  end
16
21
 
22
+ # @return [false]
17
23
  def err?
18
24
  false
19
25
  end
20
26
 
21
27
  def error
22
- raise NoErrorError.new(@status, @data)
28
+ raise AccessError, self
23
29
  end
24
30
  end
25
31
  end
@@ -0,0 +1,342 @@
1
+ require_relative 'shared_context_pipeline/errors'
2
+ require_relative 'shared_context_pipeline/router_definition'
3
+ require_relative 'shared_context_pipeline/step'
4
+ require_relative 'shared_context_pipeline/mutation_step'
5
+ require_relative 'shared_context_pipeline/track'
6
+ require_relative 'shared_context_pipeline/track_list'
7
+ require_relative 'shared_context_pipeline/wrap'
8
+ require_relative 'shared_context_pipeline/dsl'
9
+
10
+ module Flows
11
+ # Abstraction for organizing calculations in a shared data context.
12
+ #
13
+ # Let's start with example. Let's say we have to calculate `(a + b) * (a - b)`:
14
+ #
15
+ # class Claculation < Flows::SharedContextPipeline
16
+ # step :calc_left_part
17
+ # step :calc_right_part
18
+ # step :calc_result
19
+ #
20
+ # def calc_left_part(a:, b:, **)
21
+ # ok(left: a + b)
22
+ # end
23
+ #
24
+ # def calc_right_part(a:, b:, **)
25
+ # ok(right: a - b)
26
+ # end
27
+ #
28
+ # def calc_result(left:, right:, **)
29
+ # ok(result: left * right)
30
+ # end
31
+ # end
32
+ #
33
+ # x = Calculation.call(a: 1, b: 2)
34
+ # # x is a `Flows::Result::Ok`
35
+ #
36
+ # x.unwrap
37
+ # # => { a: 1, b: 2, left: 3, right: -1, result: -3 }
38
+ #
39
+ # It works by the following rules:
40
+ #
41
+ # * execution context is a Hash with Symbol keys.
42
+ # * input becomes initial execution context.
43
+ # * steps are executed in a provided order.
44
+ # * actual execution context becomes a step input.
45
+ # * step implementation is a public method with the same name.
46
+ # * step implementation must return {Flows::Result} ({Flows::Result::Helpers} already included).
47
+ # * Result Object data will be merged to shared context after each step execution.
48
+ # * If returned Result Object is successful - next step will be executed,
49
+ # in the case of the last step a calculation will be finished
50
+ # * If returned Result Object is failure - a calculation will be finished
51
+ # * When calculation is finished a Result Object will be returned:
52
+ # * result will have the same type and status as in the last executed step result
53
+ # * result wull have a full execution context as data
54
+ #
55
+ # ## Mutation Steps
56
+ #
57
+ # You may use a different step definition way:
58
+ #
59
+ # class MyClass < Flows::SharedContextPipeline
60
+ # mut_step :hello
61
+ #
62
+ # def hello(ctx)
63
+ # ctx[:result] = 'hello'
64
+ # end
65
+ # end
66
+ #
67
+ # When you use `mut_step` DSL method you define a step with different rules for implementation:
68
+ #
69
+ # * step implementation receives _one_ argument and it's your execution context in a form of a mutable Hash
70
+ # * step implementation can modify execution context
71
+ # * if step implementation returns
72
+ # * "truly" value - it makes step successful with default status `:ok`
73
+ # * "falsey" value - it makes step failure with default status `:err`
74
+ # * {Result} - it works like for standard step, but data is ignored. Only result type and status have effect.
75
+ #
76
+ # ## Tracks & Routes
77
+ #
78
+ # In some situations you may want some branching in a mix. Let's provide an example for a common problem
79
+ # when you have to do some additional steps in case of multiple types of errors.
80
+ # Let's say report to some external system:
81
+ #
82
+ # class SafeFetchComment < Flows::SharedContextPipeline
83
+ # step :fetch_post, routes(
84
+ # match_ok => :next,
85
+ # match_err => :handle_error
86
+ # )
87
+ # step :fetch_comment, routes(
88
+ # match_ok => :next,
89
+ # match_err => :handle_error
90
+ # )
91
+ #
92
+ # track :handle_error do
93
+ # step :report_to_external_system
94
+ # step :write_logs
95
+ # end
96
+ #
97
+ # # steps implementations here
98
+ # end
99
+ #
100
+ # Let's describe how `routes(...)` and `track` DSL methods work.
101
+ #
102
+ # Each step has a router. Router is defined by a hash and `router(...)` method itself is
103
+ # a (almost) shortcut to {Flows::Flow::Router::Custom} constructor. By default each step has the following
104
+ # router definition:
105
+ #
106
+ # {
107
+ # match_ok => :next, # next step is calculated by DSL, this symbol will be substituted in a final router
108
+ # match_err => :end # `:end` means stop execution
109
+ # }
110
+ #
111
+ # Hash provided in `router(...)` method will override default hash to make a final router.
112
+ #
113
+ # Because of symbols with special behavior (`:end`, `:next`) you cannot name your steps or tracks
114
+ # `:next` or `:end`. And this is totally ok because it is reserved words in Ruby.
115
+ #
116
+ # By the way, you can route not only to tracks, but also to steps. And by using `match_ok(status)` and
117
+ # `match_err(status)` you can have different routes for different statuses of successful or failure results.
118
+ #
119
+ # Steps defined inside a track are fully isolated. The simple way is to think about track as a totally
120
+ # separate pipeline. You have to explicitly enter to it. And explicitly return from it to root-level steps
121
+ # if you want to continue execution.
122
+ #
123
+ # If you feel it's too much verbose to route many steps to the same track you can do something like this:
124
+ #
125
+ # class SafeFetchComment < Flows::SharedContextPipeline
126
+ # def self.safe_step(name)
127
+ # step name, routes(
128
+ # match_ok => :next,
129
+ # match_err => :handle_error
130
+ # )
131
+ # end
132
+ #
133
+ # safe_step :fetch_post
134
+ # safe_step :fetch_comment
135
+ #
136
+ # track :handle_error do
137
+ # step :report_to_external_system
138
+ # step :write_logs
139
+ # end
140
+ #
141
+ # # steps implementations here
142
+ # end
143
+ #
144
+ # ## Simple injecting of nested pipelines
145
+ #
146
+ # If you provide some object which responds to `#call` instead of step name - this object will be used as a step body.
147
+ #
148
+ # class SubOperation < Flows::SharedContextPipeline
149
+ # step :hello
150
+ #
151
+ # def hello(**)
152
+ # ok(data: 'some data')
153
+ # end
154
+ # end
155
+ #
156
+ # class MainOperation < Flows::SharedContextPipeline
157
+ # step :init
158
+ # step SubOperation
159
+ #
160
+ # def init(**)
161
+ # ok(generated_by_init: true)
162
+ # end
163
+ # end
164
+ #
165
+ # MainOperation.call
166
+ # # => ok(generated_by_init: true, data: 'some data')
167
+ #
168
+ # You can use the same object multiple times in the same pipeline:
169
+ #
170
+ # step SubOperation
171
+ # step SubOperation
172
+ #
173
+ # If you need any input or output processing - refactor such step definition into normal step.
174
+ #
175
+ # This way has disadvantage: you cannot route to a such step because it has no explicit name.
176
+ # To handle this you can use alternative syntax:
177
+ #
178
+ # step :do_something, body: SubOperation
179
+ #
180
+ # Same features can be used with `mut_step`.
181
+ #
182
+ # This feature is primarily intended to simplify refactoring of big pipelines into smaller ones.
183
+ #
184
+ # ## Wrappers
185
+ #
186
+ # Sometimes you have to execute some steps inside SQL-transaction or something like this.
187
+ # Most frameworks allow to do it in the following approach:
188
+ #
189
+ # SQLDataBase.transaction do
190
+ # # your steps are executed here
191
+ # # special error must be executed to cancel the transaction
192
+ # end
193
+ #
194
+ # It's impossible to do with just step or track DSL. That's why `wrap` DSL method has been added.
195
+ # Let's review it on example:
196
+ #
197
+ # class MySCP < Flows::SharedContextPipeline
198
+ # step :some_preparations
199
+ # wrap :in_transaction do
200
+ # step :action_a
201
+ # step :action_b
202
+ # end
203
+ #
204
+ # def in_transaction(ctx, meta, &block)
205
+ # result = nil
206
+ #
207
+ # ActiveRecord::Base.transaction do
208
+ # result = block.call
209
+ #
210
+ # raise ActiveRecord::Rollback if result.err?
211
+ # end
212
+ #
213
+ # result
214
+ # end
215
+ #
216
+ # # step implementations here
217
+ # end
218
+ #
219
+ # `wrap` DSL method receives name and block. Inside block you can define steps and tracks.
220
+ #
221
+ # `wrap` makes an isolated track and step structure.
222
+ # You cannot route between wrapped and unwrapped steps and tracks.
223
+ # One exception - you can route to the first wrapped step.
224
+ #
225
+ # The same wrapper with the same name can be used multiple times in the same operation:
226
+ #
227
+ # class MySCP < Flows::SharedContextPipeline
228
+ # step :some_preparations
229
+ # wrap :in_transaction do
230
+ # step :action_a
231
+ # step :action_b
232
+ # end
233
+ # step :some_calculations
234
+ # wrap :in_transaction do
235
+ # step :action_c
236
+ # step :action_d
237
+ # end
238
+ #
239
+ # # ...
240
+ # end
241
+ #
242
+ # Unlike step implementations wrapper implementation has access to a shared meta (can be useful for plugins).
243
+ #
244
+ # You may think about steps and tracks inside wrapper as a nested pipeline.
245
+ # Wrapper implementation receives mutable data context, metadata and block.
246
+ # Block execution (`block.call`) returns a result object of the executed "nested pipeline".
247
+ #
248
+ # When you route to `:end` inside wrapper - you're leaving wrapper, **not** the whole pipeline.
249
+ #
250
+ # From the execution perspective wrapper is a single step. The step name is the first wrapped step name.
251
+ #
252
+ # `wrap` itself also can have overriden routes table:
253
+ #
254
+ # wrap :in_transaction, routes(match_ok => :next, match_err => :end) do
255
+ # # steps...
256
+ # end
257
+ #
258
+ # Like a step, wrapper implementation must return {Flows::Result}.
259
+ # Result is processed with the same approach as for normal step.
260
+ # **Do not modify result returned from block - build a new one if needed.
261
+ # Otherwise mutation steps can be broken.**
262
+ #
263
+ # ## Callbacks and metadata
264
+ #
265
+ # You may want to have some logic to execute before all steps, or after all, or before each, or after each.
266
+ # For example to inject generalized execution process logging.
267
+ #
268
+ # These callbacks are executed via `instance_exec` (in the context of instance).
269
+ #
270
+ # To achieve this you can use callbacks:
271
+ #
272
+ # class MySCP < Flows::SharedContextPipeline
273
+ # before_all do |klass, data, meta|
274
+ # # you can modify execution data context and metadata here
275
+ # # return value will be ignored
276
+ # end
277
+ #
278
+ # after_all do |klass, pipeline_result, data, meta|
279
+ # # you can modify execution data context and metadata here
280
+ # # you must return a final result object here
281
+ # # if no modifications needed - just return provided pipeline_result
282
+ # end
283
+ #
284
+ # before_each do |klass, step_name, data, meta|
285
+ # # you can modify execution data context and metadata here
286
+ # # return value will be ignored
287
+ # end
288
+ #
289
+ # after_each do |klass, step_name, step_result, data, meta|
290
+ # # you can modify execution data context and metadata here
291
+ # # return value will be ignored
292
+ # #
293
+ # # callback executed after context is updated with result data
294
+ # # (in the case of normal steps, mutation steps update context directly)
295
+ # #
296
+ # # DO NOT MODIFY RESULT OBJECT HERE - IT CAN BROKE MUTATION STEPS
297
+ # end
298
+ # end
299
+ #
300
+ # Metadata - is a Hash which is shared between step executions.
301
+ # This hash becomes metadata of a final {Flows::Result}.
302
+ #
303
+ # Metadata is designed to store non-business data such as execution times,
304
+ # some library specific data, and so on.
305
+ class SharedContextPipeline
306
+ extend ::Flows::Plugin::ImplicitInit
307
+
308
+ include ::Flows::Result::Helpers
309
+ extend ::Flows::Result::Helpers
310
+
311
+ extend DSL
312
+
313
+ def initialize
314
+ @__flow = self.class.tracks.to_flow(self)
315
+ end
316
+
317
+ # Executes pipeline with provided keyword arguments, returns Result Object.
318
+ #
319
+ # @return [Flows::Result]
320
+ def call(**data) # rubocop:disable Metrics/MethodLength
321
+ klass = self.class
322
+ meta = {}
323
+ context = { data: data, meta: meta, class: klass, instance: self }
324
+
325
+ klass.before_all_callbacks.each do |callback|
326
+ instance_exec(klass, data, meta, &callback)
327
+ end
328
+
329
+ flow_result = @__flow.call(nil, context: context)
330
+
331
+ final_result = flow_result.class.new(
332
+ data,
333
+ status: flow_result.status,
334
+ meta: meta
335
+ )
336
+
337
+ klass.after_all_callbacks.reduce(final_result) do |result, callback|
338
+ instance_exec(klass, result, data, meta, &callback)
339
+ end
340
+ end
341
+ end
342
+ end