flows 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +1 -0
- data/.reek.yml +42 -0
- data/.rubocop.yml +20 -7
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +0 -6
- data/Gemfile.lock +139 -74
- data/README.md +158 -364
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +47 -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 +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 +118 -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/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/README.md +2 -186
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +25 -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 +41 -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 +75 -7
- data/lib/flows/flow/node.rb +131 -0
- data/lib/flows/flow/router.rb +25 -0
- data/lib/flows/flow/router/custom.rb +54 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +20 -0
- data/lib/flows/plugin.rb +13 -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 +84 -0
- data/lib/flows/plugin/output_contract/dsl.rb +36 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -37
- data/lib/flows/railway/dsl.rb +8 -19
- 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 +160 -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 +216 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +46 -0
- data/lib/flows/shared_context_pipeline/track.rb +67 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
- data/lib/flows/util/prepend_to_class.rb +145 -0
- data/lib/flows/version.rb +1 -1
- metadata +233 -37
- 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/implicit_build.rb +0 -16
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -55
- 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/docs/railway/basic_usage.md
DELETED
@@ -1,232 +0,0 @@
|
|
1
|
-
# Railway :: Basic Usage
|
2
|
-
|
3
|
-
`Flows::Railway` is an implementation of a Railway Programming pattern. You may read about this pattern in the following articles:
|
4
|
-
|
5
|
-
* [Programming on rails: Railway Oriented Programming](http://sandordargo.com/blog/2017/09/27/railway_oriented_programming) // it's not about Ruby on Rails
|
6
|
-
* [Railway Oriented Programming: A powerful Functional Programming pattern](https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31)
|
7
|
-
* [Railway Oriented Programming in Elixir with Pattern Matching on Function Level and Pipelining](https://medium.com/elixirlabs/railway-oriented-programming-in-elixir-with-pattern-matching-on-function-level-and-pipelining-e53972cede98)
|
8
|
-
|
9
|
-
Let's review a simple task and solve it using `Flows::Railway`: you have to get a user by ID, get all user's blog posts and convert it to an array of HTML-strings. In such situation, we have to implement three parts of our task and compose it into something we can call, for example, from a Rails controller. Also, the first and third steps may fail (user not found, conversion to HTML failed). And if a step failed - we have to return failure info immediately. Let's draw this using a UML activity diagram:
|
10
|
-
|
11
|
-
```plantuml
|
12
|
-
@startuml
|
13
|
-
|Success Path|
|
14
|
-
start
|
15
|
-
-> id: Integer;
|
16
|
-
:fetch_user;
|
17
|
-
if (success?) then (yes)
|
18
|
-
-> user: User;
|
19
|
-
:get_blog_posts;
|
20
|
-
-> posts: Array<Post>;
|
21
|
-
:convert_to_html;
|
22
|
-
if (success?) then (yes)
|
23
|
-
-> posts_html: Array<String>;
|
24
|
-
stop
|
25
|
-
else (no)
|
26
|
-
|Failure|
|
27
|
-
-> message: String;
|
28
|
-
end
|
29
|
-
endif
|
30
|
-
else (no)
|
31
|
-
|Failure|
|
32
|
-
-> message: String;
|
33
|
-
end
|
34
|
-
endif
|
35
|
-
@enduml
|
36
|
-
```
|
37
|
-
|
38
|
-
And implement using `Flows::Railway`:
|
39
|
-
|
40
|
-
```ruby
|
41
|
-
class RenderUserBlogPosts
|
42
|
-
include Flows::Railway
|
43
|
-
|
44
|
-
step :fetch_user
|
45
|
-
step :get_blog_posts
|
46
|
-
step :convert_to_html
|
47
|
-
|
48
|
-
def fetch_user(id:)
|
49
|
-
user = User.find_by_id(id)
|
50
|
-
user ? ok(user: user) : err(message: "User #{id} not found")
|
51
|
-
end
|
52
|
-
|
53
|
-
def get_blog_posts(user:)
|
54
|
-
ok(posts: User.posts)
|
55
|
-
end
|
56
|
-
|
57
|
-
def convert_to_html(posts:)
|
58
|
-
posts_html = post.map(&:text).map do |text|
|
59
|
-
html = convert(text)
|
60
|
-
return err(message: "cannot convert to html: #{text}")
|
61
|
-
end
|
62
|
-
|
63
|
-
ok(posts_html: posts_html)
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
# returns String or nil
|
69
|
-
def convert(text)
|
70
|
-
# some implementation here
|
71
|
-
end
|
72
|
-
end
|
73
|
-
```
|
74
|
-
|
75
|
-
And execute it:
|
76
|
-
|
77
|
-
```ruby
|
78
|
-
# User with id = 1 exists and with id = 2 - doesn't
|
79
|
-
|
80
|
-
RenderUserBlogPosts.new.call(id: 1)
|
81
|
-
# => Flows::Result::Ok.new(posts_html: [...])
|
82
|
-
|
83
|
-
RenderUserBlogPosts.new.call(id: 2)
|
84
|
-
# => Flows::Result::Err.new(message: 'User 2 not found')
|
85
|
-
```
|
86
|
-
|
87
|
-
## Flows::Railway rules
|
88
|
-
|
89
|
-
* steps execution happens from the first to the last step
|
90
|
-
* input arguments (`Railway#call(...)`) becomes the input of the first step
|
91
|
-
* each step should return Result Object (`Flows::Result::Helpers` already included)
|
92
|
-
* if step returns failed result - execution stops and failed Result Object returned from Railway
|
93
|
-
* if step returns successful result - result data becomes arguments of the following step
|
94
|
-
* if the last step returns successful result - it becomes a result of a Railway execution
|
95
|
-
|
96
|
-
## Defining Steps
|
97
|
-
|
98
|
-
Two ways of step definition exist. First is by using an instance method:
|
99
|
-
|
100
|
-
```ruby
|
101
|
-
step :do_something
|
102
|
-
|
103
|
-
def do_something(**arguments)
|
104
|
-
# some implementation
|
105
|
-
# Result Object as return value
|
106
|
-
end
|
107
|
-
```
|
108
|
-
|
109
|
-
Second is by using lambda:
|
110
|
-
|
111
|
-
```ruby
|
112
|
-
step :do_something, ->(**arguments) { ok(some: 'data') }
|
113
|
-
```
|
114
|
-
|
115
|
-
Definition with lambda exists primarily for debugging/testing purposes. I recommend you to use method-based implementations for all your business logic. Also, this is good for consistency, readability, and maintenance. __Think about Railway as about small book: you have a "table of contents" in a form of step definitions and actual "chapters" in the same order in a form of public methods. And your private methods becomes something like "appendix".__
|
116
|
-
|
117
|
-
## Dependency Injection
|
118
|
-
|
119
|
-
By default, we search for step implementation methods in a class instance. But you may override method source and inject your own:
|
120
|
-
|
121
|
-
```ruby
|
122
|
-
class SayOk
|
123
|
-
include Flows::Railway
|
124
|
-
|
125
|
-
step :do_job
|
126
|
-
end
|
127
|
-
|
128
|
-
module Loud
|
129
|
-
extend Flows::Result::Helpers
|
130
|
-
|
131
|
-
def self.do_job
|
132
|
-
ok(text: 'OOOOKKKK!!!!')
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
module Normal
|
137
|
-
extend Flows::Result::Helpers
|
138
|
-
|
139
|
-
def self.do_job
|
140
|
-
ok(text: 'ok')
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
SayOk.new(method_source: Loud).call.unwrap
|
145
|
-
# => { text: 'OOOOKKKK!!!!' }
|
146
|
-
|
147
|
-
SayOk.new(method_source: Normal).call.unwrap
|
148
|
-
# => { text: 'ok' }
|
149
|
-
```
|
150
|
-
|
151
|
-
When you change your method source original class is no longer used for methods lookup. But what if we want to just override one of the steps? We can:
|
152
|
-
|
153
|
-
```ruby
|
154
|
-
class SayOk
|
155
|
-
include Flows::Railway
|
156
|
-
|
157
|
-
step :do_job
|
158
|
-
|
159
|
-
def do_job
|
160
|
-
ok(text: 'ok')
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
say_loud = -> { ok(text: 'OOOOKKKK!!!!') } # or anything with implemented #call method
|
165
|
-
|
166
|
-
SayOk.new.call.unwrap
|
167
|
-
# => { text: 'OOOOKKKK!!!!' }
|
168
|
-
|
169
|
-
SayOk.new(deps: { do_job: say_loud }).call.unwrap
|
170
|
-
# => { text: 'ok' }
|
171
|
-
```
|
172
|
-
|
173
|
-
Moreover, you can mix both approaches. Injecting using `deps:` has higher priority.
|
174
|
-
|
175
|
-
## Pre-building and Performance
|
176
|
-
|
177
|
-
As mentioned before, railway execution consists of two phases: build (`.new`) and run (`#call`). And the build phase is expensive. You may compare overheads when you build a railway each time:
|
178
|
-
|
179
|
-
```
|
180
|
-
$ WITH_RW=1 bin/benchmark
|
181
|
-
|
182
|
-
--------------------------------------------------
|
183
|
-
- task: A + B, one step implementation
|
184
|
-
--------------------------------------------------
|
185
|
-
Warming up --------------------------------------
|
186
|
-
Flows::Railway (build once)
|
187
|
-
30.995k i/100ms
|
188
|
-
Flows::Railway (build each time)
|
189
|
-
11.553k i/100ms
|
190
|
-
Calculating -------------------------------------
|
191
|
-
Flows::Railway (build once)
|
192
|
-
347.682k (± 2.1%) i/s - 1.767M in 5.083828s
|
193
|
-
Flows::Railway (build each time)
|
194
|
-
122.908k (± 4.2%) i/s - 623.862k in 5.085459s
|
195
|
-
|
196
|
-
Comparison:
|
197
|
-
Flows::Railway (build once): 347681.6 i/s
|
198
|
-
Flows::Railway (build each time): 122908.0 i/s - 2.83x slower
|
199
|
-
|
200
|
-
|
201
|
-
--------------------------------------------------
|
202
|
-
- task: ten steps returns successful result
|
203
|
-
--------------------------------------------------
|
204
|
-
Warming up --------------------------------------
|
205
|
-
Flows::Railway (build once)
|
206
|
-
6.130k i/100ms
|
207
|
-
Flows::Railway (build each time)
|
208
|
-
2.168k i/100ms
|
209
|
-
Calculating -------------------------------------
|
210
|
-
Flows::Railway (build once)
|
211
|
-
63.202k (± 1.6%) i/s - 318.760k in 5.044862s
|
212
|
-
Flows::Railway (build each time)
|
213
|
-
21.645k (± 3.6%) i/s - 108.400k in 5.014725s
|
214
|
-
|
215
|
-
Comparison:
|
216
|
-
Flows::Railway (build once): 63202.5 i/s
|
217
|
-
Flows::Railway (build each time): 21645.2 i/s - 2.92x slower
|
218
|
-
```
|
219
|
-
|
220
|
-
As the benchmark shows your infrastructure code overhead from Flows will be almost three times lower when you build your railways at 'compile' time. I mean something like that:
|
221
|
-
|
222
|
-
```ruby
|
223
|
-
class MyClass
|
224
|
-
MY_RAILWAY = MyRailway.new # this string will be executed on a class loading stage
|
225
|
-
|
226
|
-
def my_method
|
227
|
-
MY_RAILWAY.call
|
228
|
-
end
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
But if you don't care much about performance - build each time will be fast enough. Check out [Performance](overview/performance.md) page to see a bigger picture.
|
@@ -1,196 +0,0 @@
|
|
1
|
-
# Result Object :: Basic Usage
|
2
|
-
|
3
|
-
Result Object is a way of presenting the result of a calculation. The result may be successful or failed.
|
4
|
-
For example, if you calculate expression `a / b`:
|
5
|
-
|
6
|
-
* for `a = 6` and `b = 2` result will be successful with data `3`.
|
7
|
-
* for `a = 6` and `b = 0` result will be failed with data, for example, `"Cannot divide by zero"`.
|
8
|
-
|
9
|
-
Examples of such approach may be found in other libraries and languages:
|
10
|
-
|
11
|
-
* [Either Monad](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html) in Haskell
|
12
|
-
* [Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) in Rust
|
13
|
-
* [Faraday gem](https://www.rubydoc.info/gems/faraday/Faraday/Response) has `Faraday::Response` object which contains data and status
|
14
|
-
* [dry-rb Result Monad](https://dry-rb.org/gems/dry-monads/result/) has `Dry::Monads::Result`
|
15
|
-
|
16
|
-
So, why do you need Result Object? Why not just return `nil` on a failure or raise an error (like in the standard library)? Here are several reasons:
|
17
|
-
|
18
|
-
* raising errors and exceptions isn't a very convenient and explicit way to handle errors. Moreover, it is slow and looks like `goto`. However, it is still a good way to abort execution on an unexpected error.
|
19
|
-
* returning `nil` does not work when you have to deal with different types of errors or an error has some data payload.
|
20
|
-
* using specific Result Objects (like `Faraday::Response`) brings inconsistency - you have to learn how to deal with each new type of Result.
|
21
|
-
|
22
|
-
That's why `Flows` should have Result Object implementation. If any executable Flows entity will return Result Object with the same API - composing your app components becomes trivial. Result Objects should also be as fast and lightweight as possible.
|
23
|
-
|
24
|
-
Flows' implementation is inspired mainly by [Rust Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) and focused on following features:
|
25
|
-
|
26
|
-
* use idiomatic Ruby: no methods named with first capital letter (`Name(1, 2)`), etc.
|
27
|
-
* provide convenient helpers for `case` and `===` (case equality) for matching results and writing routing logic
|
28
|
-
* provide helpers for convenient creation of Result Objects
|
29
|
-
* Result Object may be successful (`Ok`) or failure (`Err`)
|
30
|
-
* Result Object has an status (some symbol: `:saved`, `:zero_division_error`)
|
31
|
-
* status usage is optional. Default statuses for successful and failure results are `:success` and `:failure`
|
32
|
-
* result may have metadata. Metadata is something unrelated to your business logic (execution time, for example, or some info about who created this result).
|
33
|
-
* different accessors for successful and failure results - prevents treating failure results as successful and vice versa.
|
34
|
-
|
35
|
-
## Class Diagram
|
36
|
-
|
37
|
-
Class UML diagram describing current implementation:
|
38
|
-
|
39
|
-
```plantuml
|
40
|
-
@startuml
|
41
|
-
class Flows::Result<Abstract Class> {
|
42
|
-
.. Constructor ..
|
43
|
-
{static} new(Symbol status, Hash data, Hash metadata)
|
44
|
-
.. Success checks ..
|
45
|
-
{abstract} bool ok?()
|
46
|
-
{abstract} bool err?()
|
47
|
-
.. Result data access ..
|
48
|
-
Symbol status()
|
49
|
-
{abstract} Hash unwrap()
|
50
|
-
{abstract} Hash error()
|
51
|
-
.. Metadata ..
|
52
|
-
Hash meta()
|
53
|
-
}
|
54
|
-
|
55
|
-
class Flows::Result::Ok {
|
56
|
-
true ok?()
|
57
|
-
false err?()
|
58
|
-
..
|
59
|
-
Hash unwrap()
|
60
|
-
[raise exception] error()
|
61
|
-
}
|
62
|
-
|
63
|
-
class Flows::Result::Err {
|
64
|
-
false ok?()
|
65
|
-
true err?()
|
66
|
-
..
|
67
|
-
[raise exception] unwrap()
|
68
|
-
Hash error()
|
69
|
-
}
|
70
|
-
|
71
|
-
Flows::Result --> Flows::Result::Ok
|
72
|
-
Flows::Result --> Flows::Result::Err
|
73
|
-
@enduml
|
74
|
-
```
|
75
|
-
|
76
|
-
## Creating Results
|
77
|
-
|
78
|
-
Most flexible and verbose way of creating Result Objects is creating via `.new`:
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
# Successful result with data {a: 1}
|
82
|
-
Flows::Result::Ok.new(a: 1)
|
83
|
-
|
84
|
-
# Failure result with data {msg: 'error'}
|
85
|
-
Flows::Result::Err.new(msg: 'error')
|
86
|
-
|
87
|
-
# Successful result with data {a: 1} and status `:done`
|
88
|
-
Flows::Result::Ok.new({ a: 1 }, status: :done)
|
89
|
-
|
90
|
-
# Failure result with data {msg: 'error'} and status `:http_error`
|
91
|
-
Flows::Result::Err.new({ msg: 'error' }, status: :http_error)
|
92
|
-
|
93
|
-
# Successful result with data {a: 1} and metadata `{ time: 123 }`
|
94
|
-
Flows::Result::Ok.new({ a: 1 }, meta: { time: 123 })
|
95
|
-
|
96
|
-
# Failure result with data {msg: 'error'} and metadata `{ time: 123 }`
|
97
|
-
Flows::Result::Err.new({ msg: 'error' }, meta: { time: 123 })
|
98
|
-
```
|
99
|
-
|
100
|
-
More convenient and short way is to use helpers:
|
101
|
-
|
102
|
-
```ruby
|
103
|
-
include Flows::Result::Helpers
|
104
|
-
|
105
|
-
# Successful result with data {a: 1}
|
106
|
-
ok(a: 1)
|
107
|
-
|
108
|
-
# Failure result with data {msg: 'error'}
|
109
|
-
err(msg: 'error')
|
110
|
-
|
111
|
-
# Successful result with data {a: 1} and status `:done`
|
112
|
-
ok(:done, a: 1)
|
113
|
-
|
114
|
-
# Failure result with data {msg: 'error'} and status `:http_error`
|
115
|
-
err(:http_error, msg: 'error')
|
116
|
-
```
|
117
|
-
|
118
|
-
You cannot provide metadata using helpers and it's ok: you shouldn't populate metadata in your business code.
|
119
|
-
Metadata is designed to use in library code and when you have to provide some metadata from your library - just use `.new` instead of helpers.
|
120
|
-
|
121
|
-
## Inspecting Results
|
122
|
-
|
123
|
-
Behaviour of any result object:
|
124
|
-
|
125
|
-
```ruby
|
126
|
-
result.status # returns status, example: `:success`
|
127
|
-
|
128
|
-
result.meta # returns metadata, example: `{}`
|
129
|
-
```
|
130
|
-
|
131
|
-
Behaviour specific to successful results:
|
132
|
-
|
133
|
-
```ruby
|
134
|
-
result.ok? # true
|
135
|
-
|
136
|
-
result.err? # false
|
137
|
-
|
138
|
-
result.unwrap # returns result data
|
139
|
-
|
140
|
-
result.error # raises exception
|
141
|
-
```
|
142
|
-
|
143
|
-
Behaviour specific to failure results:
|
144
|
-
|
145
|
-
```ruby
|
146
|
-
result.ok? # false
|
147
|
-
|
148
|
-
result.err? # true
|
149
|
-
|
150
|
-
result.unwrap # raises exception
|
151
|
-
|
152
|
-
result.error # returns result data
|
153
|
-
```
|
154
|
-
|
155
|
-
## Matching Results
|
156
|
-
|
157
|
-
Basic matching results using `case`:
|
158
|
-
|
159
|
-
```ruby
|
160
|
-
case result
|
161
|
-
when Flows::Result::Ok then do_job
|
162
|
-
when Flows::Results::Err then give_up
|
163
|
-
end
|
164
|
-
```
|
165
|
-
|
166
|
-
But this is too verbose. For this case helpers has methods for matching. Example above may be rewritten like this:
|
167
|
-
|
168
|
-
```ruby
|
169
|
-
include Flows::Result::Helpers
|
170
|
-
|
171
|
-
case result
|
172
|
-
when match_ok then do_job
|
173
|
-
when match_err then give_up
|
174
|
-
end
|
175
|
-
```
|
176
|
-
|
177
|
-
Moreover, you may specify status when using helper matchers:
|
178
|
-
|
179
|
-
```ruby
|
180
|
-
include Flows::Result::Helpers
|
181
|
-
|
182
|
-
case result
|
183
|
-
when match_ok(:create) then do_create
|
184
|
-
when match_ok(:update) then do_update
|
185
|
-
when match_err(:http_error) then retry
|
186
|
-
when match_err then give_up
|
187
|
-
end
|
188
|
-
```
|
189
|
-
|
190
|
-
## General Recommendations
|
191
|
-
|
192
|
-
Let's assume that you have some code returning Result Object.
|
193
|
-
|
194
|
-
* if error happened and may be handled somehow - return failure result
|
195
|
-
* if error happened and cannot be handled - raise exception to abort execution
|
196
|
-
* if you don't handle any errors for now - don't check result type and use `#unwrap` to access data. It will raise exception when called on a failure result.
|
@@ -1,139 +0,0 @@
|
|
1
|
-
# Result Object :: Do Notation
|
2
|
-
|
3
|
-
This functionality aims to simplify common control flow pattern: when you have to stop execution on a first failure and return this failure.
|
4
|
-
Do Notation inspired by [Do Notation in dry-rb](https://dry-rb.org/gems/dry-monads/do-notation/) and [Haskell do keyword](https://wiki.haskell.org/Keywords#do).
|
5
|
-
|
6
|
-
Sometimes you have to write something like this:
|
7
|
-
|
8
|
-
```ruby
|
9
|
-
class Something
|
10
|
-
include Flows::Result::Helpers
|
11
|
-
|
12
|
-
def do_job
|
13
|
-
user_result = fetch_user
|
14
|
-
return user_result if user_result.err?
|
15
|
-
|
16
|
-
data_result = fetch_data
|
17
|
-
return data_result if data_result.err?
|
18
|
-
|
19
|
-
calculation_result = calculation(user_result.unwrap[:user], data_result.unwrap)
|
20
|
-
return calculation_result if user_result.err?
|
21
|
-
|
22
|
-
ok(data: calculation_result.unwrap[:some_field])
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def fetch_user
|
28
|
-
# returns Ok or Err
|
29
|
-
end
|
30
|
-
|
31
|
-
def fetch_data
|
32
|
-
# returns Ok or Err
|
33
|
-
end
|
34
|
-
|
35
|
-
def calculation(_user, _data)
|
36
|
-
# returns Ok or Err
|
37
|
-
end
|
38
|
-
end
|
39
|
-
```
|
40
|
-
|
41
|
-
The main idea of the code above is to stop method execution and return failed Result Object if one of the sub-operations is failed. At the moment of failure.
|
42
|
-
|
43
|
-
By using Do Notation feature you may rewrite it like this:
|
44
|
-
|
45
|
-
```ruby
|
46
|
-
class SomethingWithDoNotation
|
47
|
-
include Flows::Result::Helpers
|
48
|
-
include Flows::Result::Do # enable Do Notation
|
49
|
-
|
50
|
-
do_for(:do_job) # changes behaviour of `yield` in this method
|
51
|
-
def do_job
|
52
|
-
user, = yield :user, fetch_user # yield here returns array of one element
|
53
|
-
data = yield fetch_data # yield here returns a Hash
|
54
|
-
|
55
|
-
ok(data: yield(:some_field, calculation(user, data))[0])
|
56
|
-
end
|
57
|
-
|
58
|
-
# private method definitions
|
59
|
-
end
|
60
|
-
```
|
61
|
-
|
62
|
-
or like this:
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
do_for(:do_job)
|
66
|
-
def do_job
|
67
|
-
user = yield(fetch_user)[:user] # yield here and below returns a Hash
|
68
|
-
data = yield fetch_data
|
69
|
-
|
70
|
-
ok(data: yield(calculation(user, data))[:some_field])
|
71
|
-
end
|
72
|
-
```
|
73
|
-
|
74
|
-
`do_for(:do_job)` makes some simple magic here and allows you to use `yield` inside `do_job` in a non standard way:
|
75
|
-
to unpack results or instantly leave a method if a failed result provided.
|
76
|
-
|
77
|
-
## How to use it
|
78
|
-
|
79
|
-
First of all, you have to include `Flows::Result::Do` mixin into your class or module. It adds `do_for` class method.
|
80
|
-
`do_for` accepts method name as an argument and changes behaviour of `yield` inside this method. By the way, when you are using
|
81
|
-
`do_for` you cannot pass a block to modified method anymore.
|
82
|
-
|
83
|
-
Then `do_for` method should be used to enable Do Notation for certain methods.
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
class MyClass
|
87
|
-
include Flows::Result::Do
|
88
|
-
|
89
|
-
do_for(:my_method_1)
|
90
|
-
def my_method_1
|
91
|
-
# some code
|
92
|
-
end
|
93
|
-
|
94
|
-
do_for(:my_method_2)
|
95
|
-
def my_method_2
|
96
|
-
# some code
|
97
|
-
end
|
98
|
-
end
|
99
|
-
```
|
100
|
-
|
101
|
-
`yield` in such methods starts working by following rules:
|
102
|
-
|
103
|
-
```ruby
|
104
|
-
ok_result = Flows::Result::Ok.new(a: 1, b: 2)
|
105
|
-
err_result = Flows::Result::Err.new(x: 1, y: 2)
|
106
|
-
|
107
|
-
# following three lines are equivalent
|
108
|
-
yield(ok_result)
|
109
|
-
ok_result.unwrap
|
110
|
-
{ a: 1, b: 2 }
|
111
|
-
|
112
|
-
# following three lines are equivalent
|
113
|
-
yield(:a, :b, ok_result)
|
114
|
-
ok_result.unwrap.values_at(:a, :b)
|
115
|
-
[1, 2]
|
116
|
-
|
117
|
-
# following two lines are equivalent
|
118
|
-
yield(err_result)
|
119
|
-
return err_result
|
120
|
-
|
121
|
-
# following two lines are equivalent
|
122
|
-
yield(:x, :y, err_result)
|
123
|
-
return err_result
|
124
|
-
```
|
125
|
-
|
126
|
-
As you may see, `yield` has two forms of usage:
|
127
|
-
|
128
|
-
* `yield(result_value)` - returns unwrapped data Hash for successful results or,
|
129
|
-
in case of failed result, stops method execution and returns failed `result_value` as a method result.
|
130
|
-
* `yield(*keys, result_value)` - returns unwrapped data under provided keys as Array for successful results or,
|
131
|
-
in case of failed result, stops method execution and returns failed `result_value` as a method result.
|
132
|
-
|
133
|
-
## How it works
|
134
|
-
|
135
|
-
Under the hood `Flows::Result::Do` creates a module and prepends it to your class or module.
|
136
|
-
Invoking of `do_for(:method_name)` adds special wrapper method to the prepended module. So, when you perform call to
|
137
|
-
`YourClassOrModule#method_name` - you execute wrapper in the prepended module.
|
138
|
-
|
139
|
-
Check out source code for implementation details.
|