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
@@ -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