flows 0.1.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +38 -0
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -0
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +44 -2
  7. data/.ruby-version +1 -1
  8. data/.yardopts +1 -0
  9. data/CHANGELOG.md +65 -0
  10. data/README.md +186 -256
  11. data/Rakefile +35 -1
  12. data/bin/.rubocop.yml +5 -0
  13. data/bin/all_the_errors +55 -0
  14. data/bin/benchmark +69 -78
  15. data/bin/benchmark_cli/compare.rb +118 -0
  16. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  17. data/bin/benchmark_cli/compare/base.rb +45 -0
  18. data/bin/benchmark_cli/compare/command.rb +47 -0
  19. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  20. data/bin/benchmark_cli/examples.rb +23 -0
  21. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  22. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  30. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  32. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  37. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  38. data/bin/benchmark_cli/helpers.rb +12 -0
  39. data/bin/benchmark_cli/ruby.rb +15 -0
  40. data/bin/benchmark_cli/ruby/command.rb +38 -0
  41. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  42. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  43. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  44. data/bin/console +1 -0
  45. data/bin/docserver +7 -0
  46. data/bin/errors +130 -0
  47. data/bin/errors_cli/contract_error_demo.rb +49 -0
  48. data/bin/errors_cli/di_error_demo.rb +38 -0
  49. data/bin/errors_cli/flow_error_demo.rb +22 -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/.nojekyll +0 -0
  56. data/docs/README.md +13 -0
  57. data/docs/_sidebar.md +2 -0
  58. data/docs/index.html +30 -0
  59. data/flows.gemspec +27 -2
  60. data/forspell.dict +17 -0
  61. data/lefthook.yml +21 -0
  62. data/lib/flows.rb +13 -5
  63. data/lib/flows/contract.rb +402 -0
  64. data/lib/flows/contract/array.rb +55 -0
  65. data/lib/flows/contract/case_eq.rb +43 -0
  66. data/lib/flows/contract/compose.rb +77 -0
  67. data/lib/flows/contract/either.rb +53 -0
  68. data/lib/flows/contract/error.rb +25 -0
  69. data/lib/flows/contract/hash.rb +75 -0
  70. data/lib/flows/contract/hash_of.rb +70 -0
  71. data/lib/flows/contract/helpers.rb +22 -0
  72. data/lib/flows/contract/predicate.rb +34 -0
  73. data/lib/flows/contract/transformer.rb +50 -0
  74. data/lib/flows/contract/tuple.rb +70 -0
  75. data/lib/flows/flow.rb +96 -7
  76. data/lib/flows/flow/errors.rb +29 -0
  77. data/lib/flows/flow/node.rb +132 -0
  78. data/lib/flows/flow/router.rb +29 -0
  79. data/lib/flows/flow/router/custom.rb +59 -0
  80. data/lib/flows/flow/router/errors.rb +11 -0
  81. data/lib/flows/flow/router/simple.rb +25 -0
  82. data/lib/flows/plugin.rb +14 -0
  83. data/lib/flows/plugin/dependency_injector.rb +159 -0
  84. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  85. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  86. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  87. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  88. data/lib/flows/plugin/implicit_init.rb +45 -0
  89. data/lib/flows/plugin/output_contract.rb +85 -0
  90. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  91. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  92. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  93. data/lib/flows/plugin/profiler.rb +114 -0
  94. data/lib/flows/plugin/profiler/injector.rb +35 -0
  95. data/lib/flows/plugin/profiler/report.rb +48 -0
  96. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  97. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  98. data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
  99. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  100. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  101. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  102. data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
  103. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  104. data/lib/flows/railway.rb +154 -0
  105. data/lib/flows/railway/dsl.rb +18 -0
  106. data/lib/flows/railway/errors.rb +17 -0
  107. data/lib/flows/railway/step.rb +24 -0
  108. data/lib/flows/railway/step_list.rb +38 -0
  109. data/lib/flows/result.rb +189 -2
  110. data/lib/flows/result/do.rb +172 -0
  111. data/lib/flows/result/err.rb +12 -6
  112. data/lib/flows/result/errors.rb +29 -17
  113. data/lib/flows/result/helpers.rb +25 -3
  114. data/lib/flows/result/ok.rb +12 -6
  115. data/lib/flows/shared_context_pipeline.rb +299 -0
  116. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  117. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
  118. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  119. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  120. data/lib/flows/shared_context_pipeline/mutation_step.rb +29 -0
  121. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  122. data/lib/flows/shared_context_pipeline/step.rb +44 -0
  123. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  124. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  125. data/lib/flows/shared_context_pipeline/wrap.rb +74 -0
  126. data/lib/flows/util.rb +17 -0
  127. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  128. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +98 -0
  129. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  130. data/lib/flows/util/prepend_to_class.rb +179 -0
  131. data/lib/flows/version.rb +1 -1
  132. metadata +288 -20
  133. data/.travis.yml +0 -8
  134. data/Gemfile.lock +0 -119
  135. data/bin/demo +0 -66
  136. data/bin/examples.rb +0 -159
  137. data/bin/profile_10steps +0 -64
  138. data/bin/ruby_benchmarks +0 -26
  139. data/lib/flows/node.rb +0 -27
  140. data/lib/flows/operation.rb +0 -54
  141. data/lib/flows/operation/builder.rb +0 -130
  142. data/lib/flows/operation/builder/build_router.rb +0 -37
  143. data/lib/flows/operation/dsl.rb +0 -72
  144. data/lib/flows/operation/errors.rb +0 -75
  145. data/lib/flows/operation/executor.rb +0 -78
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a69e7b183fcc1a9a0e0eabeaac0e66aff9fc55ea458fa34a5c37de86c7970ab
4
- data.tar.gz: 20dbb58536ba20c0f7c7032757679ea968088965890851e93af81e2b4cb148f4
3
+ metadata.gz: e6f029a3e03c0cd3ac484cb703f67a1c4910a10c8395e7b1024518dc84a14cb5
4
+ data.tar.gz: 23f44992d336cd637c02ff6d96c82a99f7e6af2a2affc82bcdc2c3f4ee1153a2
5
5
  SHA512:
6
- metadata.gz: b606d7df548e65d694f4700ecf6991667bca8f6626a41111bce6cbe9f8949ad0c5cfc58830fd1596b1fc3c170eaca7aed2b4f8f83092a5a7d174979978ece70b
7
- data.tar.gz: 4fbfd1e9422aca8b7800b4930d32a7a3a4f4eca67eceab08cf0b20fcde24ef628d677f5bc40ffa1f7a1c9c3af6f1f80de78a6e248adf145f23f63d3d4ae6c77c
6
+ metadata.gz: 17015a5eb9868ced9c18872706625df9aeb51aa90717be36929fe223006b5e28fa439fcc63db49b43a047c0375b3d6493f4640da6164be21815b3df95291ae96
7
+ data.tar.gz: b2170ec9d8a710b70fd676772ba7a5556ab23b9a50313bf34f321afb440f07d9fe66bc76634a9e53f02dd7063ebf92e27a94e162e6470e2b22a87703a6856f48
@@ -0,0 +1,38 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ branches:
9
+ - master
10
+ schedule:
11
+ - cron: 0 2 * * 1-5
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby:
20
+ - 2.5.x
21
+ - 2.6.x
22
+ # - 2.7.x
23
+ steps:
24
+ - uses: actions/checkout@v1
25
+ - name: Set up Ruby
26
+ uses: actions/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ - name: Install Bundler
30
+ run: gem install bundler
31
+ - name: Install Hunspell
32
+ run: sudo apt-get install hunspell
33
+ - name: Install Deps
34
+ run: bundle install --jobs 4 --retry 3
35
+ - name: Test
36
+ env:
37
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
38
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle/
2
+ /.vendor/
2
3
  /.yardoc
3
4
  /_yardoc/
4
5
  /coverage/
@@ -12,4 +13,11 @@
12
13
 
13
14
  # profile results folder
14
15
  /profile/
15
- !/profile/.keep
16
+ !/profile/.keep
17
+
18
+ # OS specific stuff
19
+ .DS_Store
20
+
21
+ # In case of gem development we don't need to track Gemfile.lock
22
+ # All the constraints must be expressed in gemspec
23
+ /Gemfile.lock
data/.mdlrc ADDED
@@ -0,0 +1 @@
1
+ rules "~MD013", "~MD033", "~MD024"
@@ -0,0 +1,54 @@
1
+ # default config: https://github.com/troessner/reek/blob/master/docs/defaults.reek.yml
2
+ # detectors' docs: https://github.com/troessner/reek/tree/master/docs
3
+ ---
4
+ exclude_paths:
5
+ - 'bin/'
6
+ - 'spec/'
7
+
8
+ detectors:
9
+ LongParameterList:
10
+ enabled: true
11
+ exclude: []
12
+ max_params: 4 # +1 to default value
13
+ overrides:
14
+ initialize:
15
+ max_params: 6 # +1 to default value
16
+ ModuleInitialize:
17
+ enabled: false
18
+ TooManyInstanceVariables:
19
+ enabled: true
20
+ exclude: []
21
+ max_instance_variables: 5 # +1 to default value
22
+ DataClump:
23
+ exclude:
24
+ - Flows::Result::Helpers
25
+ - Flows::SharedContextPipeline::DSL::Tracks
26
+ IrresponsibleModule:
27
+ exclude:
28
+ - Flows::SharedContextPipeline
29
+ FeatureEnvy:
30
+ exclude:
31
+ - Flows::SharedContextPipeline#call
32
+ - Flows::Contract # too many false positives here
33
+ - Flows::Plugin::Profiler::Report#events
34
+ TooManyStatements:
35
+ exclude:
36
+ - Flows::SharedContextPipeline#call
37
+ - Flows::Plugin::Profiler::Wrapper#make_instance_wrapper
38
+ - Flows::Plugin::Profiler::Wrapper#make_singleton_wrapper
39
+ - initialize
40
+ DuplicateMethodCall:
41
+ exclude:
42
+ - Flows::SharedContextPipeline#call
43
+ - Flows::Plugin::Profiler::Wrapper#make_instance_wrapper
44
+ - Flows::Plugin::Profiler::Wrapper#make_singleton_wrapper
45
+ - 'length'
46
+ MissingSafeMethod:
47
+ exclude:
48
+ - Flows::Contract
49
+ BooleanParameter:
50
+ exclude:
51
+ - Flows::Plugin::OutputContract::DSL
52
+ TooManyMethods:
53
+ exclude:
54
+ - Flows::Plugin::Profiler::Report::Tree::Node
@@ -1,11 +1,22 @@
1
1
  require:
2
2
  - rubocop-rspec
3
3
  - rubocop-performance
4
+ - rubocop-md
4
5
 
5
6
  AllCops:
6
- TargetRubyVersion: 2.6
7
+ TargetRubyVersion: 2.7
8
+ NewCops: enable
7
9
 
8
- LineLength:
10
+ Style/HashEachMethods:
11
+ Enabled: true
12
+
13
+ Style/HashTransformKeys:
14
+ Enabled: true
15
+
16
+ Style/HashTransformValues:
17
+ Enabled: true
18
+
19
+ Layout/LineLength:
9
20
  Max: 120
10
21
 
11
22
  Metrics/BlockLength:
@@ -20,3 +31,34 @@ Metrics/ParameterLists:
20
31
 
21
32
  Style/WhileUntilModifier:
22
33
  Enabled: false
34
+
35
+ Naming/MethodParameterName:
36
+ Exclude:
37
+ - '**/*.md'
38
+ - 'spec/**/*'
39
+
40
+ Style/MixinUsage:
41
+ Exclude:
42
+ - '**/*.md'
43
+
44
+ Lint/UnusedMethodArgument:
45
+ Exclude:
46
+ - '**/*.md'
47
+
48
+ Lint/UnusedBlockArgument:
49
+ Exclude:
50
+ - '**/*.md'
51
+
52
+ Style/CaseEquality:
53
+ Exclude:
54
+ - 'lib/flows/contract/**/*'
55
+
56
+ Naming/RescuedExceptionsVariableName:
57
+ PreferredName: err
58
+
59
+ Style/Documentation:
60
+ Exclude:
61
+ - 'lib/flows/shared_context_pipeline/step.rb' # false positive here
62
+
63
+ Style/SlicingWithRange:
64
+ Enabled: false # to support Ruby 2.5
@@ -1 +1 @@
1
- 2.6.2
1
+ 2.6.5
@@ -0,0 +1 @@
1
+ --markup markdown
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Types of changes:
9
+
10
+ * _Added_ - for new features.
11
+ * _Changed_ - for changes in existing functionality.
12
+ * _Deprecated_ - for soon-to-be removed features.
13
+ * _Removed_ - for now removed features.
14
+ * _Fixed_ -- for any bug fixes.
15
+ * _Security_ - in case of vulnerabilities.
16
+
17
+ ## [Unreleased]
18
+
19
+ ## [0.5.1] - 2020-06-29
20
+
21
+ ### Fixed
22
+
23
+ * `Flows::SharedContextPipeline` wrap DSL failure in case of inheritance, [issue](https://github.com/ffloyd/flows/issues/18)
24
+
25
+ ## [0.5.0] - 2020-05-18
26
+
27
+ ### Added
28
+
29
+ * `Flows::SharedContextPipeline` wrap DSL, [issue](https://github.com/ffloyd/flows/issues/7)
30
+ * `Flows::Flow` routing integrity check on initialization
31
+ * `Flows::Plugin::OutputContract` skip contract DSL method
32
+ * `Flows::Plugin::Profiler` introduced. Report types: raw, tree and flat.
33
+
34
+ ### Changed
35
+
36
+ * `Flows::SharedContextPipeline` callback API, [issue](https://github.com/ffloyd/flows/issues/6)
37
+ * `Flows::Util` modules API, [issue](https://github.com/ffloyd/flows/issues/11)
38
+ * `Flows::Contract::CaseEq` default error expanded to present more context
39
+
40
+ ## [0.4.0] - 2020-04-21
41
+
42
+ ### Added
43
+
44
+ * `Flows::Contract` - type contracts with specific transformation feature.
45
+ * `Flows::Flow` - fast and lightweight logic execution engine, designed for
46
+ internal usage and library writers.
47
+ * `Flows::Plugin::DependencyInjector` - simple dependency injection plugin for your classes
48
+ * `Flows::Plugin::ImplicitInit` - allows to use `MyClass.call` instead of
49
+ `MyClass.new.call`, an class instance will be created once.
50
+ * `Flows::Plugin::OutputContract` - plugin for output type checks and
51
+ transformations for service objects which return `Flows::Result`.
52
+ * `Flows::Railway` - stupid simple implementation of the railway pattern.
53
+ * `Flows::Result` - general purpose Result Object designed after Rust Result type.
54
+ * `Flows::Result::Do` - do-notation for Result Objects.
55
+ * `Flows::SharedContextPipeline` - much more flexible adoption of the railway
56
+ pattern for Ruby.
57
+ * `Flows::Util::InheritableSingletonVars` - allows to define behavior for
58
+ singleton variables in the context of inheritance.
59
+ * `Flows::Util::PrependToClass` - allows to prepend some module to class even if
60
+ target module did not included directly into class.
61
+
62
+ [unreleased]: https://github.com/ffloyd/flows/compare/v0.5.1...HEAD
63
+ [0.5.1]: https://github.com/ffloyd/flows/compare/v0.5.0...v0.5.1
64
+ [0.5.0]: https://github.com/ffloyd/flows/compare/v0.4.0...v0.5.0
65
+ [0.4.0]: https://github.com/ffloyd/flows/compare/v0.3.0...v0.4.0
data/README.md CHANGED
@@ -1,339 +1,269 @@
1
1
  # Flows
2
2
 
3
- [![Build Status](https://travis-ci.com/ffloyd/flows.svg?branch=master)](https://travis-ci.com/ffloyd/flows)
3
+ [![Build Status](https://github.com/ffloyd/flows/workflows/Test/badge.svg)](https://github.com/ffloyd/flows/actions)
4
4
  [![codecov](https://codecov.io/gh/ffloyd/flows/branch/master/graph/badge.svg)](https://codecov.io/gh/ffloyd/flows)
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 close to [Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/) and [Dry::Transaction](https://dry-rb.org/gems/dry-transaction/),
9
- but has more simpler and flexible DSL for defining operations and matching results. Also `flows` is significantly faster.
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
- `flows` has no production dependencies so you can use it with any framework.
15
+ `flows` has no production dependencies so it can be used with any framework.
12
16
 
13
17
  ## Installation
14
18
 
15
19
  Add this line to your application's Gemfile:
16
20
 
17
21
  ```ruby
18
- gem 'flows'
22
+ gem 'flows', '~> 0.5'
19
23
  ```
20
24
 
21
25
  And then execute:
22
26
 
23
- $ bundle
27
+ ```sh
28
+ bundle
29
+ ```
24
30
 
25
31
  Or install it yourself as:
26
32
 
27
- $ gem install flows
33
+ ```sh
34
+ gem install flows
35
+ ```
28
36
 
29
- ## Usage
37
+ ## Supported Ruby versions
30
38
 
31
- ### `Flows::Flow`
39
+ CI tests against last patch versions every day:
32
40
 
33
- Low-level instrument for defining execution flows. Used internally as execution engine for `Flows::Operation`.
34
- Check out source code and specs for details.
41
+ * `MRI 2.5.x`
42
+ * `MRI 2.6.x`
35
43
 
36
- ### `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.
37
46
 
38
- Result Object implementation. Inspired by [Dry::Monads::Result](https://dry-rb.org/gems/dry-monads/1.0/result/) and
39
- [Rust Result Objects](https://doc.rust-lang.org/1.30.0/book/2018-edition/ch09-02-recoverable-errors-with-result.html).
47
+ ## Usage & Documentation
40
48
 
41
- 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.
42
55
 
43
- * separate classes for successful (`Flows::Result::Ok`) and failure (`Flows::Result::Err`) results
44
- * both classes has same parent class `Flows::Result`
45
- * result data should be a `Hash` with symbol keys and any values
46
- * result has a status
47
- * default status for successful results is `:success`
48
- * default status for failure results is `:failure`
56
+ ## Development
49
57
 
50
- 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.
51
61
 
52
- ```ruby
53
- # create successful result with data {a: 1, b: 2}
54
- result_ok = Flows::Result::Ok.new(a:1, b: 2)
62
+ ### Anyone can make Flows even better
55
63
 
56
- # get `:a` from result
57
- 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.
58
67
 
59
- # get error data from result
60
- result_ok.error[:a] # raises exception
68
+ ### [Lefthook](https://github.com/Arkweid/lefthook) as a git hook manager
61
69
 
62
- # get status from result
63
- result_ok.status # :success
70
+ Installation on MacOS via Homebrew:
64
71
 
65
- # boolean flags
66
- result_ok.ok? # true
67
- result_ok.err? # false
72
+ ```sh
73
+ brew install Arkweid/lefthook/lefthook
74
+ ```
68
75
 
69
- # create successful result with data {a: 1, b: 2} and status `:custom`
70
- result_ok_custom = Flows::Result::Ok.new({ a: 1, b: 2 }, status: :custom)
76
+ Activation (in the root of the repo):
71
77
 
72
- # get status from result
73
- result_ok_custom.status # :custom
78
+ ```sh
79
+ lefthook install
80
+ ```
74
81
 
75
- # create failure result with data {a: 1, b: 2}
76
- result_err = Flows::Result::Err.new(a:1, b: 2)
82
+ Run hooks manually:
77
83
 
78
- # get `:a` from result
79
- result_err.unwrap[:a] # raises exception
84
+ ```sh
85
+ lefthook run pre-commit
86
+ lefthook run pre-push
87
+ ```
80
88
 
81
- # get error data from result
82
- result_err.error[:a] # 1
89
+ Please, never turn off the pre-commit and pre-push hooks.
83
90
 
84
- # get status from result
85
- result_err.status # :failure
91
+ ### Rubocop linter
86
92
 
87
- # boolean flags
88
- result_ok.ok? # false
89
- result_ok.err? # true
93
+ [Rubocop](https://docs.rubocop.org/en/stable/) in this setup is responsible for:
90
94
 
91
- # create failure result with data {a: 1, b: 2} and status `:custom`
92
- 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/))
93
99
 
94
- # get status from result
95
- result_err_custom.status # :custom
96
- ```
100
+ Rubocop config for library and RSpec files should be close to standard one only
101
+ with minor amount of exceptions.
97
102
 
98
- 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.
99
105
 
100
- ```ruby
101
- include Flows::Result::Helpers
106
+ Rubocop Metrics (ABC-size, method/class length, etc) must not be eased
107
+ globally. Never.
102
108
 
103
- # create successful result with data {a: 1, b: 2}
104
- result_ok = ok(a:1, b: 2)
109
+ ### Reek linter
105
110
 
106
- # create successful result with data {a: 1, b: 2} and status `:custom`
107
- result_ok_custom = ok(:custom, a: 1, b: 2)
111
+ [Ruby Reek](https://github.com/troessner/reek) is a very aggressive linter that
112
+ forces you to do a clean OOP design.
108
113
 
109
- # create failure result with data {a: 1, b: 2}
110
- result_err = err(a:1, b: 2)
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.
111
117
 
112
- # create failure result with data {a: 1, b: 2} and status `:custom`
113
- result_err_custom = err(:custom, a: 1, b: 2)
118
+ ### Rest of the linters
114
119
 
115
- # matching helpers
116
- result = ...
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)
117
124
 
118
- case result
119
- when match_ok(:custom)
120
- # matches only successful results with status :custom
121
- when match_ok
122
- # matches only successful results with any status
123
- when match_err(:custom)
124
- # matches only failure results with status :custom
125
- when match_err
126
- # matches only failure results with any status
127
- end
128
- ```
125
+ ### Default Rake task and CI
129
126
 
130
- ### `Flows::Operation`
127
+ Default rake task (`bundle exec rake`) executes the following checks:
131
128
 
132
- Let's solve simple task using operation:
129
+ * Rubocop
130
+ * Ruby Reek
131
+ * RSpec
132
+ * Spellcheck (forspell)
133
+ * MarkdownLint (mdl)
133
134
 
134
- * given numbers `a` and `b`
135
- * result should contain sum of this numbers
136
- * result should contain square of this sum
135
+ CI is also performing default Rake task. So, if you want to reproduce CI error
136
+ locally - just run `bundle exec rake`.
137
137
 
138
- ```ruby
139
- class Summator
140
- # Make this class an operation by including this module.
141
- # It adds DSL, initializer and call method.
142
- # Also it includes Flows::Result::Helper both on DSL and instance level.
143
- include Flows::Operation
144
-
145
- # This is step definitions.
146
- # In simplest form step defined by its name and
147
- # step implementation expected to be in a method
148
- # with same name.
149
- #
150
- # Steps will be executed in a definition order.
151
- step :validate
152
- step :calc_sum
153
- step :calc_square
154
-
155
- # Which keys of operation data we want to expose on success
156
- ok_shape :sum, :sum_square
157
-
158
- # Which keys of operation data we want to expose on failure
159
- err_shape :message
160
-
161
- # Step implementation receives execution context as keyword arguments.
162
- # For the first step context equals to operation arguments.
163
- #
164
- # Step implementation must return Result Object.
165
- # Result Objects's data will be merged into operation context.
166
- #
167
- # If result is successful - next step will be executed.
168
- # If not - operation terminates and returns failure.
169
- def validate(a:, b:, **)
170
- err(message: 'a is not a number') if !a.is_a?(Number)
171
- err(message: 'b is not a number') if !b.is_a?(Number)
172
-
173
- ok
174
- end
175
-
176
- def calc_sum(a:, b:, **)
177
- ok(sum: a + b)
178
- end
179
-
180
- # We may get data from previous steps because all results' data are merged to context.
181
- def calc_square(sum:, **)
182
- ok(sum_square: a * b)
183
- end
184
- end
185
-
186
-
187
- # prepare operation
188
- operation = Summator.new
189
-
190
- # execute operation
191
- result = operation.call(a: 1, b: 2)
192
-
193
- result.ok? # true
194
- result.unwrap # { sum: 3, sum_square: 9 } - only keys from success shape present
195
-
196
-
197
- result = operation.call(a: nil, b: nil)
198
-
199
- result.ok? # false
200
- result.error # { message: 'a is not a number' } - only keys from error shape present
201
- ```
138
+ Default Rake task is also executed as a pre-push git hook.
202
139
 
203
- #### Result Shapes
140
+ ### Error reporting
204
141
 
205
- 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,
206
- and `err_shape` to define shapes of failure result. Examples:
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.
207
144
 
208
- ```ruby
209
- # Set exposed keys for :success status of successful result.
210
- #
211
- # Success result will have shape like { key1: ..., key2: ... }
212
- #
213
- # If one of keys is missing in the final operation context an exception will be raised.
214
- ok_shape :key1, :key2
215
-
216
- # Set different exposed keys for different statuses.
217
- #
218
- # Operation result status is a status of last executed step result.
219
- ok_shape status1: [:key1, :key2],
220
- status2: [:key3]
221
-
222
- # Failure shapes defined in the same way:
223
- err_shape :key1, :key2
224
- err_shape status1: [:key1, :key2],
225
- status2: [:key3]
226
- ```
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:
227
148
 
228
- Operation definition should have exact one `ok_shape` DSL-call and zero or one `err_shape` DSL-call. If you want to disable shaping
229
- you can write `no_shape` DSL-call instead of shape definitions.
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
230
154
 
231
- #### Routing & Tracks
155
+ `bin/errors` is done using [GLI](https://davetron5000.github.io/gli/) library,
156
+ run `bin/errors -h` to explore possibilities.
232
157
 
233
- You define side tracks, even nested ones:
158
+ ### Performance
234
159
 
235
- ```ruby
236
- step :outer_1 # next step is outer_2
237
-
238
- track :some_track do
239
- step :inner_1 # next step is inner_2
240
- track :inner_track do
241
- step :deep_1 # next step is deep_2
242
- step :deep_2 # next step is inner_2
243
- end
244
- step :inner_2 # next step in outer_2
245
- end
246
-
247
- step :outer_2
248
- ```
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.
249
165
 
250
- In definition above tracks will not be used because there is no routes to this tracks. You may define routing like this:
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.
251
170
 
252
- ```ruby
253
- # if result is successful and has status :to_some_track - next step will be inner_1
254
- # for any other successful results - outer_2
255
- step :outer_1,
256
- match_ok(:to_some_track) => :some_track
257
-
258
- track :some_track do
259
- step :inner_1, match_err => :inner_track # redirect to inner_track on failure result
260
- track :inner_track do
261
- step :deep_1, match_ok(:some_status) => :outer_2 # you may redirect to steps too
262
- step :deep_2
263
- end
264
- step :inner_2
265
- end
266
-
267
- step :outer_2
268
- ```
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.
269
174
 
270
- #### Lambda Steps
175
+ And to compare performance overhead between different `flows` abstractions
176
+ and another alternatives a benchmarking CLI was done: `bin/benchmark`.
271
177
 
272
- You can use lambda for in-place step implementation:
178
+ This CLI is done using GLI, run `bin/benchmark -h` to explore possibilities.
273
179
 
274
- ```ruby
275
- step :name, ->(a:, b:, **) { ok(sum: a + b) }
276
- ```
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.
277
182
 
278
- #### Dependency Injection
183
+ ### Documentation
279
184
 
280
- You can override or inject step implementation on initialization:
185
+ Each public API method or module **must** be properly documented with examples
186
+ and motivation behind.
281
187
 
282
- ```ruby
283
- class Summator
284
- include Flows::Operation
285
-
286
- step :sum
287
-
288
- ok_shape :sum
289
- end
290
-
291
- summator = Summator.new(deps: {
292
- sum: ->(a:, b:, **) { ok(sum: a + b) }
293
- })
294
-
295
- summator.call(a: 1, b: 2).unwrap[:sum] # 3
296
- ```
188
+ To run documentation server locally run `bin/docserver`.
297
189
 
298
- #### Wrapping steps
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.
299
192
 
300
- You can wrap several steps with some logic:
193
+ ### Documentation Driven Development
301
194
 
302
- ```ruby
303
- step :first
304
-
305
- wrap :wrapper do
306
- step :wrapped
307
- end
308
-
309
- def wrapper(**context)
310
- # do smth
311
- result = yield # execute wrapped steps
312
- # do smth or modify result
313
- result
314
- end
315
- ```
195
+ When you about to do some work, the following guideline can lead to the best
196
+ results:
316
197
 
317
- There is routing limitation when you use wrap:
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
318
203
 
319
- * outside `wrap` block you may route to wrapped block by wrapper name (`:wrapper` in the provided example)
320
- * you may route wrapped steps only to wrapped steps in the same wrap block
321
- * you cannot route to wrapped steps from outside
204
+ Yes, it's TDD approach with documentation step prepended.
322
205
 
323
- ## Development
206
+ ### Unit test
207
+
208
+ Each public API method or module **must** be properly tested. Internal modules
209
+ can be tested indirectly through public API.
210
+
211
+ Test coverage **must** be higher than 95%.
212
+
213
+ ### Commit naming
214
+
215
+ You **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
216
+
217
+ Allowed prefixes since `v0.4.0`:
218
+
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
225
+
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)?
231
+
232
+ So, when you in such situation use the first applicable prefix in the list:
233
+ between `docs` and `refactor` - pick `refactor`.
234
+
235
+ Also, there is one more special prefix for release commits. Release commit
236
+ messages **must** look like: `release: v0.4.0`.
237
+
238
+ ### Changelog
239
+
240
+ Starting from `v0.4.0` [keep a changelog](https://keepachangelog.com/en/1.0.0/)
241
+ guideline must be met.
242
+
243
+ If you adding something - provide some lines to the unreleased section of the `CHANGELOG.md`.
244
+
245
+ ### Versioning
324
246
 
325
- 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.
247
+ The project strictly follows [SemVer](https://semver.org/spec/v2.0.0.html).
326
248
 
327
- To install this gem onto your local machine, run `bundle exec rake install`.
249
+ After `v1.0.0` even smallest backward incompatible change will bump major
250
+ version. _No exceptions._
328
251
 
329
- ## Contributing
252
+ Commit with a version bump should contain _only_ version bump and CHANGELOG.md update.
330
253
 
331
- Bug reports and pull requests are welcome on GitHub at 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.
254
+ ### GitHub Flow
332
255
 
333
- ## License
256
+ Since `v0.4.0` this repo strictly follow [GitHub
257
+ Flow](https://guides.github.com/introduction/flow/) with some additions:
334
258
 
335
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
259
+ * branch naming using dash: `improved-contexts`
260
+ * use [references to
261
+ issues](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
262
+ in commit messages and make links to issues in CHANGELOG.md
336
263
 
337
- ## Code of Conduct
264
+ ### Planned features for v1.0.0
338
265
 
339
- 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).
266
+ * validation framework
267
+ * error reporting improvements
268
+ * various plugins for SCP (tracing, benchmarking, logging, etc)
269
+ * site with guides and conventions