flows 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +43 -0
- data/.mdlrc +1 -0
- data/.rubocop.yml +25 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +80 -25
- data/README.md +170 -44
- data/bin/benchmark +65 -42
- data/bin/examples.rb +37 -1
- data/bin/profile_10steps +48 -6
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +197 -0
- data/docs/_sidebar.md +26 -0
- data/docs/contributing/benchmarks_profiling.md +3 -0
- data/docs/contributing/local_development.md +3 -0
- data/docs/flow/direct_usage.md +3 -0
- data/docs/flow/general_idea.md +3 -0
- data/docs/index.html +30 -0
- data/docs/operation/basic_usage.md +1 -0
- data/docs/operation/inject_steps.md +3 -0
- data/docs/operation/lambda_steps.md +3 -0
- data/docs/operation/result_shapes.md +3 -0
- data/docs/operation/routing_tracks.md +3 -0
- data/docs/operation/wrapping_steps.md +3 -0
- data/docs/overview/performance.md +336 -0
- data/docs/railway/basic_usage.md +232 -0
- data/docs/result_objects/basic_usage.md +196 -0
- data/docs/result_objects/do_notation.md +139 -0
- data/flows.gemspec +2 -0
- data/forspell.dict +8 -0
- data/lefthook.yml +12 -0
- data/lib/flows.rb +2 -0
- data/lib/flows/flow.rb +1 -1
- data/lib/flows/operation.rb +1 -3
- data/lib/flows/operation/builder.rb +2 -2
- data/lib/flows/operation/dsl.rb +21 -0
- data/lib/flows/railway.rb +48 -0
- data/lib/flows/railway/builder.rb +68 -0
- data/lib/flows/railway/dsl.rb +28 -0
- data/lib/flows/railway/errors.rb +21 -0
- data/lib/flows/railway/executor.rb +23 -0
- data/lib/flows/result.rb +1 -0
- data/lib/flows/result/do.rb +30 -0
- data/lib/flows/result_router.rb +1 -1
- data/lib/flows/version.rb +1 -1
- metadata +59 -3
- data/.travis.yml +0 -8
@@ -0,0 +1,196 @@
|
|
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.
|
@@ -0,0 +1,139 @@
|
|
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.
|
data/flows.gemspec
CHANGED
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
25
25
|
spec.add_development_dependency 'rake', '~> 10.0'
|
26
26
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
27
27
|
|
28
|
+
spec.add_development_dependency 'forspell', '~> 0.0.8'
|
28
29
|
spec.add_development_dependency 'rubocop'
|
30
|
+
spec.add_development_dependency 'rubocop-md'
|
29
31
|
spec.add_development_dependency 'rubocop-performance'
|
30
32
|
spec.add_development_dependency 'rubocop-rspec'
|
31
33
|
|
data/forspell.dict
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Format: one word per line. Empty lines and #-comments are supported too.
|
2
|
+
# If you want to add word with its forms, you can write 'word: example' (without quotes) on the line,
|
3
|
+
# where 'example' is existing word with the same possible forms (endings) as your word.
|
4
|
+
# Example: deduplicate: duplicate
|
5
|
+
linter
|
6
|
+
linters
|
7
|
+
matchers
|
8
|
+
superset
|
data/lefthook.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
pre-commit:
|
2
|
+
parallel: true
|
3
|
+
commands:
|
4
|
+
rubocop:
|
5
|
+
glob: "{*.rb,*.md,*.gemspec,Gemfile,Rakefile}"
|
6
|
+
run: bundle exec rubocop {staged_files}
|
7
|
+
markdownlinter:
|
8
|
+
glob: "*.md"
|
9
|
+
run: bundle exec mdl {staged_files}
|
10
|
+
forspell:
|
11
|
+
glob: "{*.md,*.rb}"
|
12
|
+
run: bundle exec forspell {staged_files}
|
data/lib/flows.rb
CHANGED
data/lib/flows/flow.rb
CHANGED
data/lib/flows/operation.rb
CHANGED
@@ -5,11 +5,9 @@ require_relative 'operation/builder'
|
|
5
5
|
require_relative 'operation/executor'
|
6
6
|
|
7
7
|
module Flows
|
8
|
-
#
|
8
|
+
# Operation DSL
|
9
9
|
module Operation
|
10
10
|
def self.included(mod)
|
11
|
-
mod.instance_variable_set(:@steps, [])
|
12
|
-
mod.instance_variable_set(:@track_path, [])
|
13
11
|
mod.extend ::Flows::Operation::DSL
|
14
12
|
end
|
15
13
|
|
data/lib/flows/operation/dsl.rb
CHANGED
@@ -4,6 +4,20 @@ module Flows
|
|
4
4
|
module DSL
|
5
5
|
attr_reader :steps, :ok_shapes, :err_shapes
|
6
6
|
|
7
|
+
def self.extended(mod, steps = nil, ok_shapes = nil, err_shapes = nil)
|
8
|
+
mod.instance_variable_set(:@steps, steps || [])
|
9
|
+
mod.instance_variable_set(:@track_path, [])
|
10
|
+
mod.instance_variable_set(:@ok_shapes, ok_shapes)
|
11
|
+
mod.instance_variable_set(:@err_shapes, err_shapes)
|
12
|
+
|
13
|
+
mod.class_exec do
|
14
|
+
def self.inherited(subclass)
|
15
|
+
::Flows::Operation::DSL.extended(subclass, steps.map(&:dup), ok_shapes, err_shapes)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
7
21
|
include Flows::Result::Helpers
|
8
22
|
|
9
23
|
def step(name, custom_body_or_routes = nil, custom_routes = nil)
|
@@ -30,6 +44,13 @@ module Flows
|
|
30
44
|
@track_path = track_path_before
|
31
45
|
end
|
32
46
|
|
47
|
+
def routes(routes_hash)
|
48
|
+
routes_hash
|
49
|
+
end
|
50
|
+
|
51
|
+
alias when_ok match_ok
|
52
|
+
alias when_err match_err
|
53
|
+
|
33
54
|
def wrap(name, custom_body = nil, &block)
|
34
55
|
@steps << make_step(name, type: :wrapper, custom_body: custom_body, block: block)
|
35
56
|
end
|