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.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +9 -1
- data/.mdlrc +1 -1
- data/.reek.yml +54 -0
- data/.rubocop.yml +26 -7
- data/.rubocop_todo.yml +27 -0
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +0 -6
- data/README.md +167 -363
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +73 -105
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +138 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/interface_error_demo.rb +17 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/README.md +3 -187
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +27 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +24 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +15 -0
- data/lib/flows/plugin/dependency_injector.rb +170 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/interface.rb +84 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -34
- data/lib/flows/railway/dsl.rb +8 -18
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +158 -16
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +342 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +55 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +191 -0
- data/lib/flows/version.rb +1 -1
- metadata +253 -38
- data/Gemfile.lock +0 -174
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -52
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
data/lib/flows/result/do.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
22
|
-
|
165
|
+
def do_notation(method_name)
|
166
|
+
prepended_mod = Util.fetch_and_prepend_module(self)
|
23
167
|
|
24
|
-
|
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
|
data/lib/flows/result/err.rb
CHANGED
@@ -1,25 +1,31 @@
|
|
1
1
|
module Flows
|
2
2
|
class Result
|
3
|
-
#
|
3
|
+
# Result Object for failure results.
|
4
|
+
#
|
5
|
+
# @see Flows::Result behaviour described here
|
4
6
|
class Err < Result
|
5
|
-
|
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
|
28
|
+
raise AccessError, self
|
23
29
|
end
|
24
30
|
end
|
25
31
|
end
|
data/lib/flows/result/errors.rb
CHANGED
@@ -1,29 +1,41 @@
|
|
1
1
|
module Flows
|
2
2
|
class Result
|
3
|
-
#
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
18
|
-
|
19
|
-
def
|
20
|
-
@
|
21
|
-
|
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
|
25
|
-
|
26
|
-
|
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
|
data/lib/flows/result/helpers.rb
CHANGED
@@ -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 = :
|
21
|
+
def ok(status = :ok, **data)
|
8
22
|
Flows::Result::Ok.new(data, status: status)
|
9
23
|
end
|
10
24
|
|
11
|
-
def
|
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
|
|
data/lib/flows/result/ok.rb
CHANGED
@@ -1,25 +1,31 @@
|
|
1
1
|
module Flows
|
2
2
|
class Result
|
3
|
-
#
|
3
|
+
# Result Object for successful results.
|
4
|
+
#
|
5
|
+
# @see Flows::Result behaviour described here
|
4
6
|
class Ok < Result
|
5
|
-
|
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
|
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
|