flows 0.3.0 → 0.4.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 → 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/README.md
CHANGED
@@ -5,8 +5,12 @@
|
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/flows.svg)](https://badge.fury.io/rb/flows)
|
6
6
|
|
7
7
|
Small and fast ruby framework for implementing railway-like operations.
|
8
|
-
By design it is close to
|
9
|
-
|
8
|
+
By design it is close to
|
9
|
+
[Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/),
|
10
|
+
[Dry::Transaction](https://dry-rb.org/gems/dry-transaction/) and Rust control
|
11
|
+
flow style.
|
12
|
+
Flows has simple and flexible DSL for defining operations and matching results.
|
13
|
+
Also `flows` is faster than Ruby's alternatives.
|
10
14
|
|
11
15
|
`flows` has no production dependencies so it can be used with any framework.
|
12
16
|
|
@@ -15,7 +19,7 @@ but has simpler and flexible DSL for defining operations and matching results. A
|
|
15
19
|
Add this line to your application's Gemfile:
|
16
20
|
|
17
21
|
```ruby
|
18
|
-
gem 'flows'
|
22
|
+
gem 'flows', '~> 0.4'
|
19
23
|
```
|
20
24
|
|
21
25
|
And then execute:
|
@@ -30,436 +34,226 @@ Or install it yourself as:
|
|
30
34
|
gem install flows
|
31
35
|
```
|
32
36
|
|
33
|
-
##
|
37
|
+
## Supported Ruby versions
|
34
38
|
|
35
|
-
|
39
|
+
CI tests against last patch versions every day:
|
36
40
|
|
37
|
-
|
38
|
-
|
41
|
+
* `MRI 2.5.x`
|
42
|
+
* `MRI 2.6.x`
|
39
43
|
|
40
|
-
|
44
|
+
`MRI 2.7.x` will be added later, right now (`2.7.1`) this version of MRI Ruby is too
|
45
|
+
unstable and produce segmentation faults inside RSpec internals.
|
41
46
|
|
42
|
-
|
43
|
-
[Rust Result Objects](https://doc.rust-lang.org/1.30.0/book/2018-edition/ch09-02-recoverable-errors-with-result.html).
|
47
|
+
## Usage & Documentation
|
44
48
|
|
45
|
-
|
49
|
+
* [YARD documentation](https://rubydoc.info/github/ffloyd/flows/master) - this
|
50
|
+
link is for master branch. You can also find YARD documentation for any released
|
51
|
+
version after `v0.4.0`. This documentation has a lot of examples, describes
|
52
|
+
motivation behind each abstraction, but lacks some guides and defined conventions.
|
53
|
+
* [Guides](https://ffloyd.github.io/flows/#/) - guides, conventions, integration
|
54
|
+
and migration notes. Will be done before `v1.0.0` release. Right now is under development.
|
46
55
|
|
47
|
-
|
48
|
-
* both classes has same parent class `Flows::Result`
|
49
|
-
* result data should be a `Hash` with symbol keys and any values
|
50
|
-
* result has a status
|
51
|
-
* default status for successful results is `:success`
|
52
|
-
* default status for failure results is `:failure`
|
56
|
+
## Development
|
53
57
|
|
54
|
-
|
58
|
+
`Flows` is designed to be framework for your business logic. It is a big
|
59
|
+
responsibility. That's why `flows` has near to be sadistic development
|
60
|
+
conventions and linter setup.
|
55
61
|
|
56
|
-
|
57
|
-
# create successful result with data {a: 1, b: 2}
|
58
|
-
result_ok = Flows::Result::Ok.new(a: 1, b: 2)
|
62
|
+
### Anyone can make Flows even better
|
59
63
|
|
60
|
-
|
61
|
-
|
64
|
+
If you see some typos or unclear things in documentation or code - feel free to open
|
65
|
+
an issue. Even if you don't have plans to implement a solution - a problem reporting
|
66
|
+
will help development much. We cannot fix what we don't know.
|
62
67
|
|
63
|
-
|
64
|
-
result_ok.error[:a] # raises exception
|
68
|
+
### [Lefthook](https://github.com/Arkweid/lefthook) as a git hook manager
|
65
69
|
|
66
|
-
|
67
|
-
result_ok.status # :success
|
70
|
+
Installation on MacOS via Homebrew:
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
+
```sh
|
73
|
+
brew install Arkweid/lefthook/lefthook
|
74
|
+
```
|
72
75
|
|
73
|
-
|
74
|
-
result_ok_custom = Flows::Result::Ok.new({ a: 1, b: 2 }, status: :custom)
|
76
|
+
Activation (in the root of the repo):
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
+
```sh
|
79
|
+
lefthook install
|
80
|
+
```
|
78
81
|
|
79
|
-
|
80
|
-
result_err = Flows::Result::Err.new(a: 1, b: 2)
|
82
|
+
Run hooks manually:
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
+
```sh
|
85
|
+
lefthook run pre-commit
|
86
|
+
lefthook run pre-push
|
87
|
+
```
|
84
88
|
|
85
|
-
|
86
|
-
result_err.error[:a] # 1
|
89
|
+
Please, never turn off the pre-commit and pre-push hooks.
|
87
90
|
|
88
|
-
|
89
|
-
result_err.status # :failure
|
91
|
+
### Rubocop linter
|
90
92
|
|
91
|
-
|
92
|
-
result_ok.ok? # false
|
93
|
-
result_ok.err? # true
|
93
|
+
[Rubocop](https://docs.rubocop.org/en/stable/) in this setup is responsible for:
|
94
94
|
|
95
|
-
|
96
|
-
|
95
|
+
* defining code style (indentation, etc.)
|
96
|
+
* suggest performance improvements ([rubocop-performance](https://docs.rubocop.org/projects/performance/en/stable/))
|
97
|
+
* forces all that stuff (with some exceptions) to snippets in Markdown files ([rubocop-md](https://github.com/rubocop-hq/rubocop-md))
|
98
|
+
* forces unit-testing best practices ([rubocop-rspec](https://docs.rubocop.org/projects/rspec/en/latest/))
|
97
99
|
|
98
|
-
|
99
|
-
|
100
|
-
```
|
100
|
+
Rubocop config for library and RSpec files should be close to standard one only
|
101
|
+
with minor amount of exceptions.
|
101
102
|
|
102
|
-
|
103
|
+
Code in Markdown snippets and `/bin` folder can ignore more rules. `/bin` folder
|
104
|
+
contains only development-related scripts and tools so it's ok to ease linter requirements.
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# create successful result with data {a: 1, b: 2}
|
108
|
-
result_ok = ok(a: 1, b: 2)
|
109
|
-
|
110
|
-
# create successful result with data {a: 1, b: 2} and status `:custom`
|
111
|
-
result_ok_custom = ok(:custom, a: 1, b: 2)
|
112
|
-
|
113
|
-
# create failure result with data {a: 1, b: 2}
|
114
|
-
result_err = err(a: 1, b: 2)
|
115
|
-
|
116
|
-
# create failure result with data {a: 1, b: 2} and status `:custom`
|
117
|
-
result_err_custom = err(:custom, a: 1, b: 2)
|
118
|
-
|
119
|
-
# matching helpers
|
120
|
-
result = SomeOperation.new.call
|
121
|
-
|
122
|
-
case result
|
123
|
-
when match_ok(:custom)
|
124
|
-
# matches only successful results with status :custom
|
125
|
-
do_something
|
126
|
-
when match_ok
|
127
|
-
# matches only successful results with any status
|
128
|
-
do_something
|
129
|
-
when match_err(:custom)
|
130
|
-
# matches only failure results with status :custom
|
131
|
-
do_something
|
132
|
-
when match_err
|
133
|
-
# matches only failure results with any status
|
134
|
-
do_something
|
135
|
-
end
|
136
|
-
```
|
106
|
+
Rubocop Metrics (ABC-size, method/class length, etc) must not be eased
|
107
|
+
globally. Never.
|
137
108
|
|
138
|
-
###
|
109
|
+
### Reek linter
|
139
110
|
|
140
|
-
|
111
|
+
[Ruby Reek](https://github.com/troessner/reek) is a very aggressive linter that
|
112
|
+
forces you to do a clean OOP design.
|
141
113
|
|
142
|
-
|
143
|
-
|
144
|
-
|
114
|
+
You will be tempted to just shut up this linter many times. But believe me, in 9
|
115
|
+
of 10 cases it worth to refactor. And after each such refactoring you will
|
116
|
+
understand OOP design better and better.
|
145
117
|
|
146
|
-
|
147
|
-
class Summator
|
148
|
-
# Make this class an operation by including this module.
|
149
|
-
# It adds DSL, initializer and call method.
|
150
|
-
# Also it includes Flows::Result::Helper both on DSL and instance level.
|
151
|
-
include Flows::Operation
|
152
|
-
|
153
|
-
# This is step definitions.
|
154
|
-
# In simplest form step defined by its name and
|
155
|
-
# step implementation expected to be in a method
|
156
|
-
# with same name.
|
157
|
-
#
|
158
|
-
# Steps will be executed in a definition order.
|
159
|
-
step :validate
|
160
|
-
step :calc_sum
|
161
|
-
step :calc_square
|
162
|
-
|
163
|
-
# Which keys of operation data we want to expose on success
|
164
|
-
ok_shape :sum, :sum_square
|
165
|
-
|
166
|
-
# Which keys of operation data we want to expose on failure
|
167
|
-
err_shape :message
|
168
|
-
|
169
|
-
# Step implementation receives execution context as keyword arguments.
|
170
|
-
# For the first step context equals to operation arguments.
|
171
|
-
#
|
172
|
-
# Step implementation must return Result Object.
|
173
|
-
# Result Objects's data will be merged into operation context.
|
174
|
-
#
|
175
|
-
# If result is successful - next step will be executed.
|
176
|
-
# If not - operation terminates and returns failure.
|
177
|
-
def validate(a:, b:, **)
|
178
|
-
err(message: 'a is not a number') unless a.is_a?(Number)
|
179
|
-
err(message: 'b is not a number') unless b.is_a?(Number)
|
180
|
-
|
181
|
-
ok
|
182
|
-
end
|
183
|
-
|
184
|
-
def calc_sum(a:, b:, **)
|
185
|
-
ok(sum: a + b)
|
186
|
-
end
|
187
|
-
|
188
|
-
# We may get data from previous steps because all results' data are merged to context.
|
189
|
-
def calc_square(sum:, **)
|
190
|
-
ok(sum_square: sum * sum)
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# prepare operation
|
195
|
-
operation = Summator.new
|
196
|
-
|
197
|
-
# execute operation
|
198
|
-
result = operation.call(a: 1, b: 2)
|
199
|
-
|
200
|
-
result.ok? # true
|
201
|
-
result.unwrap # { sum: 3, sum_square: 9 } - only keys from success shape present
|
202
|
-
|
203
|
-
result = operation.call(a: nil, b: nil)
|
204
|
-
|
205
|
-
result.ok? # false
|
206
|
-
result.error # { message: 'a is not a number' } - only keys from error shape present
|
207
|
-
```
|
118
|
+
### Rest of the linters
|
208
119
|
|
209
|
-
|
120
|
+
* [MDL](https://github.com/markdownlint/markdownlint) - for consistent format of Markdown files
|
121
|
+
* [forspell](https://github.com/kkuprikov/forspell) - for spellchecking in comments and markdown files
|
122
|
+
* [inch](http://rrrene.org/inch/) - for documentation coverage suggestions (the
|
123
|
+
only optional linter)
|
210
124
|
|
211
|
-
|
125
|
+
### Default Rake task and CI
|
212
126
|
|
213
|
-
|
214
|
-
# Set exposed keys for :success status of successful result.
|
215
|
-
#
|
216
|
-
# Success result will have shape like { key1: ..., key2: ... }
|
217
|
-
#
|
218
|
-
# If one of keys is missing in the final operation context an exception will be raised.
|
219
|
-
ok_shape :key1, :key2
|
220
|
-
|
221
|
-
# Set different exposed keys for different statuses.
|
222
|
-
#
|
223
|
-
# Operation result status is a status of last executed step result.
|
224
|
-
ok_shape status1: %i[key1 key2],
|
225
|
-
status2: [:key3]
|
226
|
-
|
227
|
-
# Failure shapes defined in the same way:
|
228
|
-
err_shape :key1, :key2
|
229
|
-
err_shape status1: %i[key1 key2],
|
230
|
-
status2: [:key3]
|
231
|
-
```
|
127
|
+
Default rake task (`bundle exec rake`) executes the following checks:
|
232
128
|
|
233
|
-
|
234
|
-
|
129
|
+
* Rubocop
|
130
|
+
* Ruby Reek
|
131
|
+
* RSpec
|
132
|
+
* Spellcheck (forspell)
|
133
|
+
* MarkdownLint (mdl)
|
235
134
|
|
236
|
-
|
135
|
+
CI is also performing default Rake task. So, if you want to reproduce CI error
|
136
|
+
locally - just run `bundle exec rake`.
|
237
137
|
|
238
|
-
|
138
|
+
Default Rake task is also executed as a pre-push git hook.
|
239
139
|
|
240
|
-
|
241
|
-
step :outer_1 # next step is outer_2
|
242
|
-
|
243
|
-
track :some_track do
|
244
|
-
step :inner_1 # next step is inner_2
|
245
|
-
track :inner_track do
|
246
|
-
step :deep_1 # next step is deep_2
|
247
|
-
step :deep_2 # next step is inner_2
|
248
|
-
end
|
249
|
-
step :inner_2 # next step in outer_2
|
250
|
-
end
|
251
|
-
|
252
|
-
step :outer_2
|
253
|
-
```
|
140
|
+
### Error reporting
|
254
141
|
|
255
|
-
|
142
|
+
I hope no one will argue that clear errors makes development noticeably faster.
|
143
|
+
That's why _each_ exception in `flows` should be clear and easy to read.
|
256
144
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
step :outer_1, routes(
|
261
|
-
when_ok(:to_some_track) => :some_track
|
262
|
-
)
|
263
|
-
|
264
|
-
track :some_track do
|
265
|
-
step :inner * 1, routes(when_err => :inner_track) # redirect to inner_track on any failure result
|
266
|
-
track :inner_track do
|
267
|
-
step :deep_1, routes(when_ok(:some_status) => :outer_2) # you may redirect to steps too
|
268
|
-
step :deep_2
|
269
|
-
end
|
270
|
-
step :inner_2
|
271
|
-
end
|
272
|
-
|
273
|
-
step :outer_2
|
274
|
-
```
|
145
|
+
This cannot be tested automatically: you only can test correctness
|
146
|
+
automatically, convenience can only be tested manually. That's why when you
|
147
|
+
introduce any new `raise` you have to:
|
275
148
|
|
276
|
-
|
149
|
+
* make an error message clear and descriptive
|
150
|
+
* add this error to _errors demo CLI_ (`bin/errors`)
|
151
|
+
* add this errors to _all the errors demo_ (`bin/all_the_errors`)
|
152
|
+
* make sure that error is displayed correctly and follows a style of the rest
|
153
|
+
of implemented errors
|
277
154
|
|
278
|
-
|
279
|
-
|
280
|
-
match_ok(:status) => :track_name,
|
281
|
-
match_ok => :track_name
|
282
|
-
```
|
155
|
+
`bin/errors` is done using [GLI](https://davetron5000.github.io/gli/) library,
|
156
|
+
run `bin/errors -h` to explore possibilities.
|
283
157
|
|
284
|
-
|
158
|
+
### Performance
|
285
159
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
```
|
160
|
+
Ruby is slow. Moreover, Ruby is very slow. Yes, again. In the past time we had
|
161
|
+
to compare Ruby with Python. Python was faster and that's why people started to
|
162
|
+
complain about Ruby performance. That was fixed. But is Ruby fast nowadays? No.
|
163
|
+
Because languages like Clojure, Go, Rust, Elixir appeared and in comparison
|
164
|
+
with any of these languages Ruby is very very slow.
|
292
165
|
|
293
|
-
|
166
|
+
That's why you **must** be extra careful with performance. Some business
|
167
|
+
operations can be executed hundreds or even thousands times per request. Each
|
168
|
+
line of code in your abstraction will slow down such request a bit. That's why
|
169
|
+
you should think about each line performance.
|
294
170
|
|
295
|
-
|
171
|
+
Also, it's nearly impossible to make zero-cost abstractions in Ruby. The best
|
172
|
+
thing you can do - to offload calculations to a class loading or initialization
|
173
|
+
step. Sacrifice some warm-up time to make runtime performance better.
|
296
174
|
|
297
|
-
|
175
|
+
And to compare performance overhead between different `flows` abstractions
|
176
|
+
and another alternatives a benchmarking CLI was done: `bin/benchmark`.
|
298
177
|
|
299
|
-
|
300
|
-
step :name, ->(a:, b:, **) { ok(sum: a + b) }
|
301
|
-
```
|
302
|
-
|
303
|
-
#### Dependency Injection
|
178
|
+
This CLI is done using GLI, run `bin/benchmark -h` to explore possibilities.
|
304
179
|
|
305
|
-
|
306
|
-
|
307
|
-
```ruby
|
308
|
-
class Summator
|
309
|
-
include Flows::Operation
|
180
|
+
So far, `flows` offers the best performance among alternatives. And this CLI
|
181
|
+
is made to simplify comparison with alternatives and keep `flows` the fastest solution.
|
310
182
|
|
311
|
-
|
183
|
+
### Documentation
|
312
184
|
|
313
|
-
|
314
|
-
|
185
|
+
Each public API method or module **must** be properly documented with examples
|
186
|
+
and motivation behind.
|
315
187
|
|
316
|
-
|
317
|
-
sum: ->(a:, b:, **) { ok(sum: a + b) }
|
318
|
-
})
|
188
|
+
To run documentation server locally run `bin/docserver`.
|
319
189
|
|
320
|
-
|
321
|
-
|
190
|
+
Respect `@since` YARD documentation tag. When some module, class or method has any
|
191
|
+
API change - you have to provide correct `@since` tag value to the documentation.
|
322
192
|
|
323
|
-
|
193
|
+
### Documentation Driven Development
|
324
194
|
|
325
|
-
|
195
|
+
When you about to do some work, the following guideline can lead to the best
|
196
|
+
results:
|
326
197
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
end
|
333
|
-
|
334
|
-
def wrapper(**_context)
|
335
|
-
# do smth
|
336
|
-
result = yield # execute wrapped steps
|
337
|
-
# do smth or modify result
|
338
|
-
result
|
339
|
-
end
|
340
|
-
```
|
198
|
+
* first, write needed class and method structure without implementation
|
199
|
+
* write YARD documentation with motivation and usage examples for each public
|
200
|
+
class, method, module.
|
201
|
+
* write unit tests, check that tests are failing
|
202
|
+
* write implementation until tests are green
|
341
203
|
|
342
|
-
|
204
|
+
Yes, it's TDD approach with documentation step prepended.
|
343
205
|
|
344
|
-
|
345
|
-
* you may route wrapped steps only to wrapped steps in the same wrap block
|
346
|
-
* you cannot route to wrapped steps from outside
|
206
|
+
### Unit test
|
347
207
|
|
348
|
-
|
208
|
+
Each public API method or module **must** be properly tested. Internal modules
|
209
|
+
can be tested indirectly through public API.
|
349
210
|
|
350
|
-
|
211
|
+
Test coverage **must** be higher than 95%.
|
351
212
|
|
352
|
-
|
213
|
+
### Commit naming
|
353
214
|
|
354
|
-
|
215
|
+
You **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
355
216
|
|
356
|
-
|
357
|
-
operation = OperationClass.new
|
217
|
+
Allowed prefixes since `v0.4.0`:
|
358
218
|
|
359
|
-
|
360
|
-
|
219
|
+
* `feat:` - for new features
|
220
|
+
* `fix:` - for bugfixes
|
221
|
+
* `perf:` - for performance improvements
|
222
|
+
* `refactor:` - for refactoring work
|
223
|
+
* `ci:` - updates for CI configuration
|
224
|
+
* `docs:` - for documentation update
|
361
225
|
|
362
|
-
|
226
|
+
Sometimes commit can have several responsibilities. As example: when you write
|
227
|
+
documentation, test and implementation for a feature in the one commit. You can do
|
228
|
+
extra effort to split and rearrange commits to make it atomic. But does it
|
229
|
+
really provide significant value if we already have a strong convention for
|
230
|
+
changelog (see the next section)?
|
363
231
|
|
364
|
-
|
365
|
-
|
366
|
-
```
|
232
|
+
So, when you in such situation use the first applicable prefix in the list:
|
233
|
+
between `docs` and `refactor` - pick `refactor`.
|
367
234
|
|
368
|
-
|
369
|
-
|
370
|
-
### Benchmark Results
|
371
|
-
|
372
|
-
Host:
|
373
|
-
|
374
|
-
* MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
|
375
|
-
* 3.1 GHz Intel Core i5
|
376
|
-
* 8 GB 2133 MHz LPDDR3
|
377
|
-
|
378
|
-
Results:
|
379
|
-
|
380
|
-
```text
|
381
|
-
--------------------------------------------------
|
382
|
-
- task: A + B, one step implementation
|
383
|
-
--------------------------------------------------
|
384
|
-
Warming up --------------------------------------
|
385
|
-
Flows::Operation (build each time)
|
386
|
-
9.147k i/100ms
|
387
|
-
Flows::Operation (build once)
|
388
|
-
25.738k i/100ms
|
389
|
-
Dry::Transaction (build each time)
|
390
|
-
2.294k i/100ms
|
391
|
-
Dry::Transaction (build once)
|
392
|
-
21.836k i/100ms
|
393
|
-
Trailblazer::Operation
|
394
|
-
5.057k i/100ms
|
395
|
-
Calculating -------------------------------------
|
396
|
-
Flows::Operation (build each time)
|
397
|
-
96.095k (± 2.3%) i/s - 484.791k in 5.047684s
|
398
|
-
Flows::Operation (build once)
|
399
|
-
281.248k (± 1.7%) i/s - 1.416M in 5.034728s
|
400
|
-
Dry::Transaction (build each time)
|
401
|
-
23.683k (± 1.7%) i/s - 119.288k in 5.038506s
|
402
|
-
Dry::Transaction (build once)
|
403
|
-
237.379k (± 3.3%) i/s - 1.201M in 5.066073s
|
404
|
-
Trailblazer::Operation
|
405
|
-
52.676k (± 1.5%) i/s - 268.021k in 5.089306s
|
406
|
-
|
407
|
-
Comparison:
|
408
|
-
Flows::Operation (build once): 281248.4 i/s
|
409
|
-
Dry::Transaction (build once): 237378.7 i/s - 1.18x slower
|
410
|
-
Flows::Operation (build each time): 96094.9 i/s - 2.93x slower
|
411
|
-
Trailblazer::Operation: 52676.3 i/s - 5.34x slower
|
412
|
-
Dry::Transaction (build each time): 23682.9 i/s - 11.88x slower
|
413
|
-
|
414
|
-
|
415
|
-
--------------------------------------------------
|
416
|
-
- task: ten steps returns successful result
|
417
|
-
--------------------------------------------------
|
418
|
-
Warming up --------------------------------------
|
419
|
-
Flows::Operation (build each time)
|
420
|
-
1.496k i/100ms
|
421
|
-
Flows::Operation (build once)
|
422
|
-
3.847k i/100ms
|
423
|
-
Dry::Transaction (build each time)
|
424
|
-
274.000 i/100ms
|
425
|
-
Dry::Transaction (build once)
|
426
|
-
2.992k i/100ms
|
427
|
-
Trailblazer::Operation
|
428
|
-
1.082k i/100ms
|
429
|
-
Calculating -------------------------------------
|
430
|
-
Flows::Operation (build each time)
|
431
|
-
15.013k (± 3.8%) i/s - 76.296k in 5.089734s
|
432
|
-
Flows::Operation (build once)
|
433
|
-
39.239k (± 1.6%) i/s - 196.197k in 5.001538s
|
434
|
-
Dry::Transaction (build each time)
|
435
|
-
2.743k (± 3.7%) i/s - 13.700k in 5.002847s
|
436
|
-
Dry::Transaction (build once)
|
437
|
-
30.441k (± 1.8%) i/s - 152.592k in 5.014565s
|
438
|
-
Trailblazer::Operation
|
439
|
-
11.022k (± 1.4%) i/s - 55.182k in 5.007543s
|
440
|
-
|
441
|
-
Comparison:
|
442
|
-
Flows::Operation (build once): 39238.6 i/s
|
443
|
-
Dry::Transaction (build once): 30440.5 i/s - 1.29x slower
|
444
|
-
Flows::Operation (build each time): 15012.7 i/s - 2.61x slower
|
445
|
-
Trailblazer::Operation: 11022.1 i/s - 3.56x slower
|
446
|
-
Dry::Transaction (build each time): 2743.0 i/s - 14.30x slower
|
447
|
-
```
|
235
|
+
Also, there is one more special prefix for release commits. Release commit
|
236
|
+
messages **must** look like: `release: v0.4.0`.
|
448
237
|
|
449
|
-
|
238
|
+
### Changelog
|
450
239
|
|
451
|
-
|
240
|
+
Starting from `v0.4.0` [keep a changelog](https://keepachangelog.com/en/1.0.0/)
|
241
|
+
guideline must be met.
|
452
242
|
|
453
|
-
|
243
|
+
If you adding something - provide some lines to the unreleased section of the `CHANGELOG.md`.
|
454
244
|
|
455
|
-
|
245
|
+
### Versioning
|
456
246
|
|
457
|
-
|
247
|
+
The project strictly follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
458
248
|
|
459
|
-
|
249
|
+
After `v1.0.0` even smallest backward incompatible change will bump major
|
250
|
+
version. _No exceptions._
|
460
251
|
|
461
|
-
|
252
|
+
Commit with a version bump should contain _only_ version bump and CHANGELOG.md update.
|
462
253
|
|
463
|
-
|
254
|
+
### Planned features for v1.0.0
|
464
255
|
|
465
|
-
|
256
|
+
* validation framework
|
257
|
+
* error reporting improvements
|
258
|
+
* various plugins for SCP (tracing, benchmarking, logging, etc)
|
259
|
+
* site with guides and conventions
|