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.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +1 -0
  4. data/.reek.yml +42 -0
  5. data/.rubocop.yml +20 -7
  6. data/.ruby-version +1 -1
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +0 -6
  10. data/Gemfile.lock +139 -74
  11. data/README.md +158 -364
  12. data/Rakefile +35 -1
  13. data/bin/.rubocop.yml +5 -0
  14. data/bin/all_the_errors +47 -0
  15. data/bin/benchmark +73 -105
  16. data/bin/benchmark_cli/compare.rb +118 -0
  17. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  18. data/bin/benchmark_cli/compare/base.rb +45 -0
  19. data/bin/benchmark_cli/compare/command.rb +47 -0
  20. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  21. data/bin/benchmark_cli/examples.rb +23 -0
  22. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  38. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  39. data/bin/benchmark_cli/helpers.rb +12 -0
  40. data/bin/benchmark_cli/ruby.rb +15 -0
  41. data/bin/benchmark_cli/ruby/command.rb +38 -0
  42. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  43. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  44. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  45. data/bin/console +1 -0
  46. data/bin/docserver +7 -0
  47. data/bin/errors +118 -0
  48. data/bin/errors_cli/contract_error_demo.rb +49 -0
  49. data/bin/errors_cli/di_error_demo.rb +38 -0
  50. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  51. data/bin/errors_cli/oc_error_demo.rb +40 -0
  52. data/bin/errors_cli/railway_error_demo.rb +10 -0
  53. data/bin/errors_cli/result_error_demo.rb +13 -0
  54. data/bin/errors_cli/scp_error_demo.rb +17 -0
  55. data/docs/README.md +2 -186
  56. data/docs/_sidebar.md +0 -24
  57. data/docs/index.html +1 -1
  58. data/flows.gemspec +25 -2
  59. data/forspell.dict +9 -0
  60. data/lefthook.yml +9 -0
  61. data/lib/flows.rb +11 -5
  62. data/lib/flows/contract.rb +402 -0
  63. data/lib/flows/contract/array.rb +55 -0
  64. data/lib/flows/contract/case_eq.rb +41 -0
  65. data/lib/flows/contract/compose.rb +77 -0
  66. data/lib/flows/contract/either.rb +53 -0
  67. data/lib/flows/contract/error.rb +25 -0
  68. data/lib/flows/contract/hash.rb +75 -0
  69. data/lib/flows/contract/hash_of.rb +70 -0
  70. data/lib/flows/contract/helpers.rb +22 -0
  71. data/lib/flows/contract/predicate.rb +34 -0
  72. data/lib/flows/contract/transformer.rb +50 -0
  73. data/lib/flows/contract/tuple.rb +70 -0
  74. data/lib/flows/flow.rb +75 -7
  75. data/lib/flows/flow/node.rb +131 -0
  76. data/lib/flows/flow/router.rb +25 -0
  77. data/lib/flows/flow/router/custom.rb +54 -0
  78. data/lib/flows/flow/router/errors.rb +11 -0
  79. data/lib/flows/flow/router/simple.rb +20 -0
  80. data/lib/flows/plugin.rb +13 -0
  81. data/lib/flows/plugin/dependency_injector.rb +159 -0
  82. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  83. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  84. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  85. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  86. data/lib/flows/plugin/implicit_init.rb +45 -0
  87. data/lib/flows/plugin/output_contract.rb +84 -0
  88. data/lib/flows/plugin/output_contract/dsl.rb +36 -0
  89. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  90. data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
  91. data/lib/flows/railway.rb +140 -37
  92. data/lib/flows/railway/dsl.rb +8 -19
  93. data/lib/flows/railway/errors.rb +8 -12
  94. data/lib/flows/railway/step.rb +24 -0
  95. data/lib/flows/railway/step_list.rb +38 -0
  96. data/lib/flows/result.rb +188 -2
  97. data/lib/flows/result/do.rb +160 -16
  98. data/lib/flows/result/err.rb +12 -6
  99. data/lib/flows/result/errors.rb +29 -17
  100. data/lib/flows/result/helpers.rb +25 -3
  101. data/lib/flows/result/ok.rb +12 -6
  102. data/lib/flows/shared_context_pipeline.rb +216 -0
  103. data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
  104. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  105. data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
  106. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  107. data/lib/flows/shared_context_pipeline/step.rb +46 -0
  108. data/lib/flows/shared_context_pipeline/track.rb +67 -0
  109. data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
  110. data/lib/flows/util.rb +17 -0
  111. data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
  112. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
  113. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
  114. data/lib/flows/util/prepend_to_class.rb +145 -0
  115. data/lib/flows/version.rb +1 -1
  116. metadata +233 -37
  117. data/bin/demo +0 -66
  118. data/bin/examples.rb +0 -195
  119. data/bin/profile_10steps +0 -106
  120. data/bin/ruby_benchmarks +0 -26
  121. data/docs/CNAME +0 -1
  122. data/docs/contributing/benchmarks_profiling.md +0 -3
  123. data/docs/contributing/local_development.md +0 -3
  124. data/docs/flow/direct_usage.md +0 -3
  125. data/docs/flow/general_idea.md +0 -3
  126. data/docs/operation/basic_usage.md +0 -1
  127. data/docs/operation/inject_steps.md +0 -3
  128. data/docs/operation/lambda_steps.md +0 -3
  129. data/docs/operation/result_shapes.md +0 -3
  130. data/docs/operation/routing_tracks.md +0 -3
  131. data/docs/operation/wrapping_steps.md +0 -3
  132. data/docs/overview/performance.md +0 -336
  133. data/docs/railway/basic_usage.md +0 -232
  134. data/docs/result_objects/basic_usage.md +0 -196
  135. data/docs/result_objects/do_notation.md +0 -139
  136. data/lib/flows/implicit_build.rb +0 -16
  137. data/lib/flows/node.rb +0 -27
  138. data/lib/flows/operation.rb +0 -55
  139. data/lib/flows/operation/builder.rb +0 -130
  140. data/lib/flows/operation/builder/build_router.rb +0 -37
  141. data/lib/flows/operation/dsl.rb +0 -93
  142. data/lib/flows/operation/errors.rb +0 -75
  143. data/lib/flows/operation/executor.rb +0 -78
  144. data/lib/flows/railway/builder.rb +0 -68
  145. data/lib/flows/railway/executor.rb +0 -23
  146. data/lib/flows/result_router.rb +0 -14
  147. 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 [Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/) and [Dry::Transaction](https://dry-rb.org/gems/dry-transaction/),
9
- but has simpler and flexible DSL for defining operations and matching results. Also `flows` is faster, see [Performance](#performance).
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
- ## Usage
37
+ ## Supported Ruby versions
34
38
 
35
- ### `Flows::Flow`
39
+ CI tests against last patch versions every day:
36
40
 
37
- Low-level instrument for defining execution flows. Used internally as execution engine for `Flows::Operation`.
38
- Check out source code and specs for details.
41
+ * `MRI 2.5.x`
42
+ * `MRI 2.6.x`
39
43
 
40
- ### `Flows::Result`
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
- Result Object implementation. Inspired by [Dry::Monads::Result](https://dry-rb.org/gems/dry-monads/1.0/result/) and
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
- Main concepts & conventions:
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
- * separate classes for successful (`Flows::Result::Ok`) and failure (`Flows::Result::Err`) results
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
- Basic usage:
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
- ```ruby
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
- # get `:a` from result
61
- result_ok.unwrap[:a] # 1
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
- # get error data from result
64
- result_ok.error[:a] # raises exception
68
+ ### [Lefthook](https://github.com/Arkweid/lefthook) as a git hook manager
65
69
 
66
- # get status from result
67
- result_ok.status # :success
70
+ Installation on MacOS via Homebrew:
68
71
 
69
- # boolean flags
70
- result_ok.ok? # true
71
- result_ok.err? # false
72
+ ```sh
73
+ brew install Arkweid/lefthook/lefthook
74
+ ```
72
75
 
73
- # create successful result with data {a: 1, b: 2} and status `:custom`
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
- # get status from result
77
- result_ok_custom.status # :custom
78
+ ```sh
79
+ lefthook install
80
+ ```
78
81
 
79
- # create failure result with data {a: 1, b: 2}
80
- result_err = Flows::Result::Err.new(a: 1, b: 2)
82
+ Run hooks manually:
81
83
 
82
- # get `:a` from result
83
- result_err.unwrap[:a] # raises exception
84
+ ```sh
85
+ lefthook run pre-commit
86
+ lefthook run pre-push
87
+ ```
84
88
 
85
- # get error data from result
86
- result_err.error[:a] # 1
89
+ Please, never turn off the pre-commit and pre-push hooks.
87
90
 
88
- # get status from result
89
- result_err.status # :failure
91
+ ### Rubocop linter
90
92
 
91
- # boolean flags
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
- # create failure result with data {a: 1, b: 2} and status `:custom`
96
- result_err_custom = Flows::Result::Err.new({ a: 1, b: 2 }, status: :custom)
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
- # get status from result
99
- result_err_custom.status # :custom
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
- Mixin `Flows::Result::Helpers` contains tools for simpler generating and matching Result Objects:
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
- ```ruby
105
- include Flows::Result::Helpers
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
- ### `Flows::Operation`
109
+ ### Reek linter
139
110
 
140
- Let's solve simple task using operation:
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
- * given numbers `a` and `b`
143
- * result should contain sum of this numbers
144
- * result should contain square of this sum
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
- ```ruby
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
- #### Result Shapes
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
- You may limit list of exposed fields by defining success and failure shapes. _After_ step definitions use `ok_shape` to define shapes of success result, and `err_shape` to define shapes of failure result. Examples:
125
+ ### Default Rake task and CI
212
126
 
213
- ```ruby
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
- Operation definition should have exact one `ok_shape` DSL-call and zero or one `err_shape` DSL-call. If you want to disable shaping
234
- you can write `no_shape` DSL-call instead of shape definitions.
129
+ * Rubocop
130
+ * Ruby Reek
131
+ * RSpec
132
+ * Spellcheck (forspell)
133
+ * MarkdownLint (mdl)
235
134
 
236
- #### Routing & Tracks
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
- You define side tracks, even nested ones:
138
+ Default Rake task is also executed as a pre-push git hook.
239
139
 
240
- ```ruby
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
- In definition above tracks will not be used because there is no routes to this tracks. You may define routing like this:
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
- ```ruby
258
- # if result is successful and has status :to_some_track - next step will be inner_1
259
- # for any other successful results - outer_2
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
- You also can use less verbose, but shorter form of definition:
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
- ```ruby
279
- step :name,
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
- Step has default routes:
158
+ ### Performance
285
159
 
286
- ```ruby
287
- routes(
288
- when_ok => next_step_name,
289
- when_err => :term
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
- Custom routes have bigger priority than default ones. Moreover, default routes can be overriden.
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
- #### Lambda Steps
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
- You can use lambda for in-place step implementation:
175
+ And to compare performance overhead between different `flows` abstractions
176
+ and another alternatives a benchmarking CLI was done: `bin/benchmark`.
298
177
 
299
- ```ruby
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
- You can override or inject step implementation on initialization:
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
- step :sum
183
+ ### Documentation
312
184
 
313
- ok_shape :sum
314
- end
185
+ Each public API method or module **must** be properly documented with examples
186
+ and motivation behind.
315
187
 
316
- summator = Summator.new(deps: {
317
- sum: ->(a:, b:, **) { ok(sum: a + b) }
318
- })
188
+ To run documentation server locally run `bin/docserver`.
319
189
 
320
- summator.call(a: 1, b: 2).unwrap[:sum] # 3
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
- #### Wrapping steps
193
+ ### Documentation Driven Development
324
194
 
325
- You can wrap several steps with some logic:
195
+ When you about to do some work, the following guideline can lead to the best
196
+ results:
326
197
 
327
- ```ruby
328
- step :first
329
-
330
- wrap :wrapper do
331
- step :wrapped
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
- There is routing limitation when you use wrap:
204
+ Yes, it's TDD approach with documentation step prepended.
343
205
 
344
- * outside `wrap` block you may route to wrapped block by wrapper name (`:wrapper` in the provided example)
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
- ## Performance
208
+ Each public API method or module **must** be properly tested. Internal modules
209
+ can be tested indirectly through public API.
349
210
 
350
- You can compare performance for some cases by executing `bin/benchmark`. Examples for benchmark are presented in `bin/examples.rb`.
211
+ Test coverage **must** be higher than 95%.
351
212
 
352
- `Flows::Operation` and `Dry::Trancation` may be executed in two ways:
213
+ ### Commit naming
353
214
 
354
- _Build once:_ when we create operation instance once (build operation):
215
+ You **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
355
216
 
356
- ```ruby
357
- operation = OperationClass.new
217
+ Allowed prefixes since `v0.4.0`:
358
218
 
359
- 10_000.times { operation.call }
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
- _Build each time:_ when we create operation instance each execution:
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
- ```ruby
365
- 10_000.times { OperationClass.new.call }
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
- `flows` and `dry` are much faster in _build once_ way of using. Note that Trailblazer gives you only one way to execute operation.
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
- ## Development
238
+ ### Changelog
450
239
 
451
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
240
+ Starting from `v0.4.0` [keep a changelog](https://keepachangelog.com/en/1.0.0/)
241
+ guideline must be met.
452
242
 
453
- To install this gem onto your local machine, run `bundle exec rake install`.
243
+ If you adding something - provide some lines to the unreleased section of the `CHANGELOG.md`.
454
244
 
455
- ## Contributing
245
+ ### Versioning
456
246
 
457
- Bug reports and pull requests are welcome on GitHub at [ffloyd/fflows](https://github.com/ffloyd/flows). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
247
+ The project strictly follows [SemVer](https://semver.org/spec/v2.0.0.html).
458
248
 
459
- ## License
249
+ After `v1.0.0` even smallest backward incompatible change will bump major
250
+ version. _No exceptions._
460
251
 
461
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
252
+ Commit with a version bump should contain _only_ version bump and CHANGELOG.md update.
462
253
 
463
- ## Code of Conduct
254
+ ### Planned features for v1.0.0
464
255
 
465
- Everyone interacting in the Flows project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ffloyd/flows/blob/master/CODE_OF_CONDUCT.md).
256
+ * validation framework
257
+ * error reporting improvements
258
+ * various plugins for SCP (tracing, benchmarking, logging, etc)
259
+ * site with guides and conventions