flows 0.1.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://
|
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
|
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
|