flows 0.1.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +38 -0
- data/.gitignore +9 -1
- data/.mdlrc +1 -0
- data/.reek.yml +54 -0
- data/.rubocop.yml +44 -2
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +65 -0
- data/README.md +186 -256
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +69 -78
- 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 +130 -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/flow_error_demo.rb +22 -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/.nojekyll +0 -0
- data/docs/README.md +13 -0
- data/docs/_sidebar.md +2 -0
- data/docs/index.html +30 -0
- data/flows.gemspec +27 -2
- data/forspell.dict +17 -0
- data/lefthook.yml +21 -0
- data/lib/flows.rb +13 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -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 +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +14 -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 +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +154 -0
- data/lib/flows/railway/dsl.rb +18 -0
- data/lib/flows/railway/errors.rb +17 -0
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +189 -2
- data/lib/flows/result/do.rb +172 -0
- 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 +299 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +29 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +44 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +74 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +98 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +179 -0
- data/lib/flows/version.rb +1 -1
- metadata +288 -20
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -119
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -159
- data/bin/profile_10steps +0 -64
- data/bin/ruby_benchmarks +0 -26
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -54
- 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 -72
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6f029a3e03c0cd3ac484cb703f67a1c4910a10c8395e7b1024518dc84a14cb5
|
|
4
|
+
data.tar.gz: 23f44992d336cd637c02ff6d96c82a99f7e6af2a2affc82bcdc2c3f4ee1153a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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"
|
data/.reek.yml
ADDED
|
@@ -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
|
data/.rubocop.yml
CHANGED
|
@@ -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.
|
|
7
|
+
TargetRubyVersion: 2.7
|
|
8
|
+
NewCops: enable
|
|
7
9
|
|
|
8
|
-
|
|
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
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.6.
|
|
1
|
+
2.6.5
|
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--markup markdown
|
data/CHANGELOG.md
ADDED
|
@@ -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
|
-
[](https://github.com/ffloyd/flows/actions)
|
|
4
4
|
[](https://codecov.io/gh/ffloyd/flows)
|
|
5
5
|
[](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
|
|
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
|
-
`flows` has no production dependencies so
|
|
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
|
-
|
|
27
|
+
```sh
|
|
28
|
+
bundle
|
|
29
|
+
```
|
|
24
30
|
|
|
25
31
|
Or install it yourself as:
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
```sh
|
|
34
|
+
gem install flows
|
|
35
|
+
```
|
|
28
36
|
|
|
29
|
-
##
|
|
37
|
+
## Supported Ruby versions
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
CI tests against last patch versions every day:
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
* `MRI 2.5.x`
|
|
42
|
+
* `MRI 2.6.x`
|
|
35
43
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
result_ok.error[:a] # raises exception
|
|
68
|
+
### [Lefthook](https://github.com/Arkweid/lefthook) as a git hook manager
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
result_ok.status # :success
|
|
70
|
+
Installation on MacOS via Homebrew:
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
```sh
|
|
73
|
+
brew install Arkweid/lefthook/lefthook
|
|
74
|
+
```
|
|
68
75
|
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
78
|
+
```sh
|
|
79
|
+
lefthook install
|
|
80
|
+
```
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
result_err = Flows::Result::Err.new(a:1, b: 2)
|
|
82
|
+
Run hooks manually:
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
```sh
|
|
85
|
+
lefthook run pre-commit
|
|
86
|
+
lefthook run pre-push
|
|
87
|
+
```
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
result_err.error[:a] # 1
|
|
89
|
+
Please, never turn off the pre-commit and pre-push hooks.
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
result_err.status # :failure
|
|
91
|
+
### Rubocop linter
|
|
86
92
|
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
106
|
+
Rubocop Metrics (ABC-size, method/class length, etc) must not be eased
|
|
107
|
+
globally. Never.
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
result_ok = ok(a:1, b: 2)
|
|
109
|
+
### Reek linter
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
result_err_custom = err(:custom, a: 1, b: 2)
|
|
118
|
+
### Rest of the linters
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
Default rake task (`bundle exec rake`) executes the following checks:
|
|
131
128
|
|
|
132
|
-
|
|
129
|
+
* Rubocop
|
|
130
|
+
* Ruby Reek
|
|
131
|
+
* RSpec
|
|
132
|
+
* Spellcheck (forspell)
|
|
133
|
+
* MarkdownLint (mdl)
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
### Error reporting
|
|
204
141
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
155
|
+
`bin/errors` is done using [GLI](https://davetron5000.github.io/gli/) library,
|
|
156
|
+
run `bin/errors -h` to explore possibilities.
|
|
232
157
|
|
|
233
|
-
|
|
158
|
+
### Performance
|
|
234
159
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
175
|
+
And to compare performance overhead between different `flows` abstractions
|
|
176
|
+
and another alternatives a benchmarking CLI was done: `bin/benchmark`.
|
|
271
177
|
|
|
272
|
-
|
|
178
|
+
This CLI is done using GLI, run `bin/benchmark -h` to explore possibilities.
|
|
273
179
|
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
183
|
+
### Documentation
|
|
279
184
|
|
|
280
|
-
|
|
185
|
+
Each public API method or module **must** be properly documented with examples
|
|
186
|
+
and motivation behind.
|
|
281
187
|
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
### Documentation Driven Development
|
|
301
194
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
The project strictly follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
326
248
|
|
|
327
|
-
|
|
249
|
+
After `v1.0.0` even smallest backward incompatible change will bump major
|
|
250
|
+
version. _No exceptions._
|
|
328
251
|
|
|
329
|
-
|
|
252
|
+
Commit with a version bump should contain _only_ version bump and CHANGELOG.md update.
|
|
330
253
|
|
|
331
|
-
|
|
254
|
+
### GitHub Flow
|
|
332
255
|
|
|
333
|
-
|
|
256
|
+
Since `v0.4.0` this repo strictly follow [GitHub
|
|
257
|
+
Flow](https://guides.github.com/introduction/flow/) with some additions:
|
|
334
258
|
|
|
335
|
-
|
|
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
|
-
|
|
264
|
+
### Planned features for v1.0.0
|
|
338
265
|
|
|
339
|
-
|
|
266
|
+
* validation framework
|
|
267
|
+
* error reporting improvements
|
|
268
|
+
* various plugins for SCP (tracing, benchmarking, logging, etc)
|
|
269
|
+
* site with guides and conventions
|