flows 0.1.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +38 -0
- data/.gitignore +9 -1
- data/.mdlrc +1 -0
- data/.reek.yml +54 -0
- data/.rubocop.yml +44 -2
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +65 -0
- data/README.md +186 -256
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +69 -78
- 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 +19 -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 +130 -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/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/.nojekyll +0 -0
- data/docs/README.md +13 -0
- data/docs/_sidebar.md +2 -0
- data/docs/index.html +30 -0
- data/flows.gemspec +27 -2
- data/forspell.dict +17 -0
- data/lefthook.yml +21 -0
- data/lib/flows.rb +13 -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 +25 -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 +14 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -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 +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -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 +81 -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 +35 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +154 -0
- data/lib/flows/railway/dsl.rb +18 -0
- data/lib/flows/railway/errors.rb +17 -0
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +189 -2
- data/lib/flows/result/do.rb +172 -0
- 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 +299 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -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 +29 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +44 -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 +74 -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 +98 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +179 -0
- data/lib/flows/version.rb +1 -1
- metadata +288 -20
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -119
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -159
- data/bin/profile_10steps +0 -64
- data/bin/ruby_benchmarks +0 -26
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -54
- 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 -72
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -0,0 +1,18 @@
|
|
1
|
+
module Flows
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
|
+
module DSL
|
5
|
+
attr_reader :steps
|
6
|
+
|
7
|
+
SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
|
8
|
+
'@steps' => StepList.new
|
9
|
+
)
|
10
|
+
|
11
|
+
include SingletonVarsSetup
|
12
|
+
|
13
|
+
def step(name, lambda = nil)
|
14
|
+
steps.add(name: name, lambda: lambda)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flows
|
2
|
+
class Railway
|
3
|
+
# Base class for Railway errors
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Raised when initializing Railway with no steps
|
7
|
+
class NoStepsError < Error
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"No steps defined for #{@klass}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flows
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
|
+
Step = Struct.new(:name, :lambda, :next_step, keyword_init: true) do
|
5
|
+
NODE_PREPROCESSOR = ->(input, _, _) { [[], input.unwrap] }
|
6
|
+
|
7
|
+
NODE_POSTPROCESSOR = lambda do |output, context, meta|
|
8
|
+
context[:last_step] = meta[:name]
|
9
|
+
|
10
|
+
output
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_node(method_source)
|
14
|
+
Flows::Flow::Node.new(
|
15
|
+
body: lambda || method_source.method(name),
|
16
|
+
router: Flows::Flow::Router::Simple.new(next_step || :end, :end),
|
17
|
+
meta: { name: name },
|
18
|
+
preprocessor: NODE_PREPROCESSOR,
|
19
|
+
postprocessor: NODE_POSTPROCESSOR
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Flows
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
|
+
class StepList
|
5
|
+
def initialize
|
6
|
+
@list = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize_dup(_other)
|
10
|
+
@list = @list.map(&:dup)
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(name:, lambda: nil)
|
14
|
+
step = Step.new(name: name, lambda: lambda)
|
15
|
+
last_step = @list.last
|
16
|
+
|
17
|
+
last_step.next_step = name if last_step
|
18
|
+
|
19
|
+
@list << step
|
20
|
+
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def first_step_name
|
25
|
+
@list.first.name
|
26
|
+
end
|
27
|
+
|
28
|
+
# `:reek:FeatureEnvy` is false positive here.
|
29
|
+
def to_node_map(method_source)
|
30
|
+
@list.map { |step| [step.name, step.to_node(method_source)] }.to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
@list.empty?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/flows/result.rb
CHANGED
@@ -1,11 +1,197 @@
|
|
1
1
|
module Flows
|
2
|
-
#
|
2
|
+
# @abstract
|
3
|
+
#
|
4
|
+
# Result Object is a way of presenting the result of a calculation. The result may be successful or failed.
|
5
|
+
#
|
6
|
+
# For example, if you calculate expression `a / b`:
|
7
|
+
#
|
8
|
+
# * for `a = 6` and `b = 2` result will be successful with data `3`.
|
9
|
+
# * for `a = 6` and `b = 0` result will be failed with data, for example, `"Cannot divide by zero"`.
|
10
|
+
#
|
11
|
+
# Examples of such approach may be found in other libraries and languages:
|
12
|
+
#
|
13
|
+
# * [Either Monad](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html)
|
14
|
+
# in Haskell.
|
15
|
+
# * [Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) in Rust.
|
16
|
+
# * [Faraday gem](https://www.rubydoc.info/gems/faraday/Faraday/Response) has `Faraday::Response` object
|
17
|
+
# which contains data and status.
|
18
|
+
# * [dry-rb Result Monad](https://dry-rb.org/gems/dry-monads/1.3/result/) has `Dry::Monads::Result`.
|
19
|
+
#
|
20
|
+
# So, why do you need Result Object?
|
21
|
+
# Why not just return `nil` on a failure or raise an error (like in the standard library)?
|
22
|
+
# Here are several reasons:
|
23
|
+
#
|
24
|
+
# * Raising errors and exceptions is a [bad way](https://martinfowler.com/articles/replaceThrowWithNotification.html)
|
25
|
+
# of handling errors.
|
26
|
+
# Moreover, it is slow and looks like `goto`.
|
27
|
+
# However, it is still a good way to abort execution on an unexpected error.
|
28
|
+
# * Returning `nil` does not work when you have to deal with different types of errors or
|
29
|
+
# an error has some data payload.
|
30
|
+
# * Using specific Result Objects (like `Faraday::Response`) brings inconsistency -
|
31
|
+
# you have to learn how to deal with each new type of Result.
|
32
|
+
#
|
33
|
+
# That's why `Flows` should have Result Object implementation.
|
34
|
+
# If any executable Flows entity will return Result Object with the same API -
|
35
|
+
# composing your app components becomes trivial.
|
36
|
+
# Result Objects should also be as fast and lightweight as possible.
|
37
|
+
#
|
38
|
+
# Flows' implementation is inspired mainly by [Rust Result Type](https://doc.rust-lang.org/std/result/enum.Result.html)
|
39
|
+
# and focused on following features:
|
40
|
+
#
|
41
|
+
# * Use idiomatic Ruby: no methods named with first capital letter (`Name(1, 2)`), etc.
|
42
|
+
# * Use `case` and `===` (case equality) for matching results and writing routing logic.
|
43
|
+
# * Provide helpers for convenient creation and matching of Result Objects ({Helpers}).
|
44
|
+
# * Result Object may be successful ({Ok}) or failure ({Err}).
|
45
|
+
# * Result Object has an {#status} (some symbol: `:saved`, `:zero_division_error`).
|
46
|
+
# * Status usage is optional. Default statuses for successful and failure results are `:ok` and `:err`.
|
47
|
+
# * Result may have metadata ({#meta}).
|
48
|
+
# Metadata is something unrelated to your business logic
|
49
|
+
# (execution time, for example, or some info about who created this result).
|
50
|
+
# This data must not be used in business logic, it's for a library code.
|
51
|
+
# * Different accessors for successful and failure results -
|
52
|
+
# prevents treating failure results as successful and vice versa.
|
53
|
+
#
|
54
|
+
# ## General Recommendations
|
55
|
+
#
|
56
|
+
# Let's assume that you have some code returning Result Object.
|
57
|
+
#
|
58
|
+
# * if an error happened and may be handled somehow - return failure result.
|
59
|
+
# * if an error happened and cannot be handled - raise exception to abort execution.
|
60
|
+
# * if you don't handle any errors for now - don't check result type and
|
61
|
+
# use {#unwrap} to access data. It will raise exception when called on a failure result.
|
62
|
+
#
|
63
|
+
# @example Creating Result Objects
|
64
|
+
# # Successful result with data {a: 1}
|
65
|
+
# x = Flows::Result::Ok.new(a: 1)
|
66
|
+
#
|
67
|
+
# # Failure result with data {msg: 'error'}
|
68
|
+
# x = Flows::Result::Err.new(msg: 'error')
|
69
|
+
#
|
70
|
+
# # Successful result with data {a: 1} and status `:done`
|
71
|
+
# x = Flows::Result::Ok.new({ a: 1 }, status: :done)
|
72
|
+
#
|
73
|
+
# # Failure result with data {msg: 'error'} and status `:http_error`
|
74
|
+
# x = Flows::Result::Err.new({ msg: 'error' }, status: :http_error)
|
75
|
+
#
|
76
|
+
# # Successful result with data {a: 1} and metadata { time: 123 }
|
77
|
+
# x = Flows::Result::Ok.new({ a: 1 }, meta: { time: 123 })
|
78
|
+
#
|
79
|
+
# # Failure result with data {msg: 'error'} and metadata { time: 123 }
|
80
|
+
# x = Flows::Result::Err.new({ msg: 'error' }, meta: { time: 123 })
|
81
|
+
#
|
82
|
+
# @example Create Result Objects using helpers
|
83
|
+
# class Demo
|
84
|
+
# # You cannot provide metadata using helpers and it's ok:
|
85
|
+
# # you shouldn't populate metadata in your business code.
|
86
|
+
# # Metadata is designed to use in library code and
|
87
|
+
# # when you have to provide some metadata from your library -
|
88
|
+
# # just use `.new` instead of helpers.
|
89
|
+
# include Flows::Result::Helpers
|
90
|
+
#
|
91
|
+
# def demo
|
92
|
+
# # Successful result with data {a: 1}
|
93
|
+
# x = ok(a: 1)
|
94
|
+
#
|
95
|
+
# # Failure result with data {msg: 'error'}
|
96
|
+
# x = err(msg: 'error')
|
97
|
+
#
|
98
|
+
# # Successful result with data {a: 1} and status `:done`
|
99
|
+
# x = ok(:done, a: 1)
|
100
|
+
#
|
101
|
+
# # Failure result with data {msg: 'error'} and status `:http_error`
|
102
|
+
# x = err(:http_error, msg: 'error')
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# @example Inspecting Result Objects
|
107
|
+
# # Behaviour of any result object:
|
108
|
+
# result.status # returns status, example: `:ok`
|
109
|
+
# result.meta # returns metadata, example: `{}`
|
110
|
+
#
|
111
|
+
# # Behaviour specific to successful results:
|
112
|
+
# result.ok? # true
|
113
|
+
# result.err? # false
|
114
|
+
# result.unwrap # returns result data
|
115
|
+
# result.error # raises exception
|
116
|
+
#
|
117
|
+
# # Behaviour specific to failure results:
|
118
|
+
# result.ok? # false
|
119
|
+
# result.err? # true
|
120
|
+
# result.unwrap # raises exception
|
121
|
+
# result.error # returns result data
|
122
|
+
#
|
123
|
+
# @example Matching Results with case
|
124
|
+
# case result
|
125
|
+
# when Flows::Result::Ok then do_job
|
126
|
+
# when Flows::Result::Err then give_up
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# @example Matching Results with case and helpers
|
130
|
+
# class Demo
|
131
|
+
# include Flows::Result::Helpers
|
132
|
+
#
|
133
|
+
# def simple_usage
|
134
|
+
# case result
|
135
|
+
# when match_ok then do_job
|
136
|
+
# when match_err then give_up
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# def with_status_matching
|
141
|
+
# case result
|
142
|
+
# when match_ok(:create) then do_create
|
143
|
+
# when match_ok(:update) then do_update
|
144
|
+
# when match_err(:http_error) then retry
|
145
|
+
# when match_err then give_up
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# @!method ok?
|
151
|
+
# @abstract
|
152
|
+
# @return [Boolean] `true` if result is successful
|
153
|
+
# @!method err?
|
154
|
+
# @abstract
|
155
|
+
# @return [Boolean] `true` if result is failure
|
156
|
+
# @!method unwrap
|
157
|
+
# @abstract
|
158
|
+
# @return [Object] result data
|
159
|
+
# @raise [AccessError] if called on failure object
|
160
|
+
# @!method error
|
161
|
+
# @abstract
|
162
|
+
# @return [Object] result data
|
163
|
+
# @raise [AccessError] if called on successful object
|
164
|
+
#
|
165
|
+
# @since 0.4.0
|
3
166
|
class Result
|
4
|
-
|
167
|
+
# @return [Symbol] status of Result Object, default is `:ok` for successful results
|
168
|
+
# and `:err` for failure results.
|
169
|
+
attr_reader :status
|
5
170
|
|
171
|
+
# @return [Hash] metadata, don't use it to store business data
|
172
|
+
attr_reader :meta
|
173
|
+
|
174
|
+
# Direct creation of this abstract class is forbidden.
|
175
|
+
#
|
176
|
+
# @raise [StandardError] you will get an error
|
6
177
|
def initialize(**)
|
7
178
|
raise 'Use Flows::Result::Ok or Flows::Result::Err for build result objects'
|
8
179
|
end
|
180
|
+
|
181
|
+
# Results are equal if have same type and data.
|
182
|
+
#
|
183
|
+
# Metadata is ignored in comparison.
|
184
|
+
#
|
185
|
+
# @return [Boolean]
|
186
|
+
def ==(other)
|
187
|
+
return false if self.class != other.class
|
188
|
+
|
189
|
+
(status == other.status) && (data == other.send(:data))
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
attr_accessor :data
|
9
195
|
end
|
10
196
|
end
|
11
197
|
|
@@ -13,3 +199,4 @@ require_relative 'result/errors'
|
|
13
199
|
require_relative 'result/ok'
|
14
200
|
require_relative 'result/err'
|
15
201
|
require_relative 'result/helpers'
|
202
|
+
require_relative 'result/do'
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Flows
|
2
|
+
class Result
|
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.
|
124
|
+
module Do
|
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
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def do_notation(method_name)
|
166
|
+
prepended_mod = Util.fetch_and_prepend_module(self)
|
167
|
+
|
168
|
+
Util.define_wrapper(prepended_mod, method_name)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|