flows 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +43 -0
- data/.mdlrc +1 -0
- data/.rubocop.yml +25 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +80 -25
- data/README.md +170 -44
- data/bin/benchmark +65 -42
- data/bin/examples.rb +37 -1
- data/bin/profile_10steps +48 -6
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +197 -0
- data/docs/_sidebar.md +26 -0
- data/docs/contributing/benchmarks_profiling.md +3 -0
- data/docs/contributing/local_development.md +3 -0
- data/docs/flow/direct_usage.md +3 -0
- data/docs/flow/general_idea.md +3 -0
- data/docs/index.html +30 -0
- data/docs/operation/basic_usage.md +1 -0
- data/docs/operation/inject_steps.md +3 -0
- data/docs/operation/lambda_steps.md +3 -0
- data/docs/operation/result_shapes.md +3 -0
- data/docs/operation/routing_tracks.md +3 -0
- data/docs/operation/wrapping_steps.md +3 -0
- data/docs/overview/performance.md +336 -0
- data/docs/railway/basic_usage.md +232 -0
- data/docs/result_objects/basic_usage.md +196 -0
- data/docs/result_objects/do_notation.md +139 -0
- data/flows.gemspec +2 -0
- data/forspell.dict +8 -0
- data/lefthook.yml +12 -0
- data/lib/flows.rb +2 -0
- data/lib/flows/flow.rb +1 -1
- data/lib/flows/operation.rb +1 -3
- data/lib/flows/operation/builder.rb +2 -2
- data/lib/flows/operation/dsl.rb +21 -0
- data/lib/flows/railway.rb +48 -0
- data/lib/flows/railway/builder.rb +68 -0
- data/lib/flows/railway/dsl.rb +28 -0
- data/lib/flows/railway/errors.rb +21 -0
- data/lib/flows/railway/executor.rb +23 -0
- data/lib/flows/result.rb +1 -0
- data/lib/flows/result/do.rb +30 -0
- data/lib/flows/result_router.rb +1 -1
- data/lib/flows/version.rb +1 -1
- metadata +59 -3
- data/.travis.yml +0 -8
data/bin/benchmark
CHANGED
@@ -6,44 +6,62 @@ require 'benchmark/ips'
|
|
6
6
|
|
7
7
|
require_relative './examples'
|
8
8
|
|
9
|
+
with_all = ENV['WITH_ALL']
|
10
|
+
|
11
|
+
with_railway = ENV['WITH_RW'] || with_all
|
12
|
+
with_operation = ENV['WITH_OP'] || with_all
|
13
|
+
with_dry = ENV['WITH_DRY'] || with_all
|
14
|
+
with_trailblazer = ENV['WITH_TB'] || with_all
|
15
|
+
|
16
|
+
with_poro = ENV['WITH_PORO']
|
17
|
+
|
18
|
+
no_prebuild = ENV['NO_PREBUILD']
|
19
|
+
no_eachbuild = ENV['NO_EACHBUILD']
|
20
|
+
|
21
|
+
|
9
22
|
puts '-' * 50
|
10
23
|
puts '- task: A + B, one step implementation'
|
11
24
|
puts '-' * 50
|
12
25
|
|
13
26
|
flows_summator = FlowsSummator.new
|
27
|
+
flows_railway_summator = FlowsRailwaySummator.new
|
14
28
|
dry_summator = DrySummator.new
|
15
29
|
|
16
30
|
Benchmark.ips do |b|
|
17
|
-
b.report 'Flows::
|
18
|
-
|
19
|
-
end
|
31
|
+
b.report 'Flows::Railway (build once)' do
|
32
|
+
flows_railway_summator.call(a: 1, b: 2)
|
33
|
+
end if with_railway && !no_prebuild
|
34
|
+
|
35
|
+
b.report 'Flows::Railway (build each time)' do
|
36
|
+
FlowsRailwaySummator.new.call(a: 1, b: 2)
|
37
|
+
end if with_railway && !no_eachbuild
|
20
38
|
|
21
39
|
b.report 'Flows::Operation (build once)' do
|
22
40
|
flows_summator.call(a: 1, b: 2)
|
23
|
-
end
|
41
|
+
end if with_operation && !no_prebuild
|
24
42
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
43
|
+
b.report 'Flows::Operation (build each time)' do
|
44
|
+
FlowsSummator.new.call(a: 1, b: 2)
|
45
|
+
end if with_operation && !no_eachbuild
|
46
|
+
|
47
|
+
b.report 'Dry::Transaction (build once)' do
|
48
|
+
dry_summator.call(a: 1, b: 2)
|
49
|
+
end if with_dry && !no_prebuild
|
29
50
|
|
30
|
-
|
31
|
-
|
32
|
-
|
51
|
+
b.report 'Dry::Transaction (build each time)' do
|
52
|
+
DrySummator.new.call(a: 1, b: 2)
|
53
|
+
end if with_dry && !no_eachbuild
|
33
54
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
55
|
+
b.report 'Trailblazer::Operation' do
|
56
|
+
TBSummator.call(a: 1, b: 2)
|
57
|
+
end if with_trailblazer
|
38
58
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
59
|
+
b.report 'PORO' do
|
60
|
+
POROSummator.call(a: 1, b: 2)
|
61
|
+
end if with_poro
|
44
62
|
|
45
63
|
b.compare!
|
46
|
-
end
|
64
|
+
end
|
47
65
|
puts
|
48
66
|
|
49
67
|
|
@@ -52,37 +70,42 @@ puts '- task: ten steps returns successful result'
|
|
52
70
|
puts '-' * 50
|
53
71
|
|
54
72
|
flows_ten_steps = FlowsTenSteps.new
|
73
|
+
flows_railway_ten_steps = FlowsRailwayTenSteps.new
|
55
74
|
dry_ten_steps = DryTenSteps.new
|
56
75
|
|
57
76
|
Benchmark.ips do |b|
|
58
|
-
b.report 'Flows::
|
59
|
-
|
60
|
-
end
|
77
|
+
b.report 'Flows::Railway (build once)' do
|
78
|
+
flows_railway_ten_steps.call(a: 1, b: 2)
|
79
|
+
end if with_railway && !no_prebuild
|
80
|
+
|
81
|
+
b.report 'Flows::Railway (build each time)' do
|
82
|
+
FlowsRailwayTenSteps.new.call(a: 1, b: 2)
|
83
|
+
end if with_railway && !no_eachbuild
|
61
84
|
|
62
85
|
b.report 'Flows::Operation (build once)' do
|
63
86
|
flows_ten_steps.call(a: 1, b: 2)
|
64
|
-
end
|
87
|
+
end if with_operation && !no_prebuild
|
88
|
+
|
89
|
+
b.report 'Flows::Operation (build each time)' do
|
90
|
+
FlowsTenSteps.new.call(a: 1, b: 2)
|
91
|
+
end if with_operation && !no_eachbuild
|
65
92
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
93
|
+
b.report 'Dry::Transaction (build once)' do
|
94
|
+
dry_ten_steps.call(a: 1, b: 2)
|
95
|
+
end if with_dry && !no_prebuild
|
70
96
|
|
71
|
-
|
72
|
-
|
73
|
-
|
97
|
+
b.report 'Dry::Transaction (build each time)' do
|
98
|
+
DryTenSteps.new.call(a: 1, b: 2)
|
99
|
+
end if with_dry && !no_eachbuild
|
74
100
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
101
|
+
b.report 'Trailblazer::Operation' do
|
102
|
+
TBTenSteps.call(a: 1, b: 2)
|
103
|
+
end if with_trailblazer
|
79
104
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|
105
|
+
b.report 'PORO' do
|
106
|
+
POROTenSteps.call
|
107
|
+
end if with_poro
|
85
108
|
|
86
109
|
b.compare!
|
87
|
-
end
|
110
|
+
end
|
88
111
|
puts
|
data/bin/examples.rb
CHANGED
@@ -19,6 +19,16 @@ class FlowsSummator
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
class FlowsRailwaySummator
|
23
|
+
include Flows::Railway
|
24
|
+
|
25
|
+
step :sum
|
26
|
+
|
27
|
+
def sum(a:, b:)
|
28
|
+
ok(sum: a + b)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
22
32
|
class POROSummator
|
23
33
|
def self.call(a:, b:)
|
24
34
|
a + b
|
@@ -46,7 +56,7 @@ class TBSummator < Trailblazer::Operation
|
|
46
56
|
end
|
47
57
|
|
48
58
|
#
|
49
|
-
# Task: 10 steps which
|
59
|
+
# Task: 10 steps which returns simple value
|
50
60
|
#
|
51
61
|
|
52
62
|
class FlowsTenSteps
|
@@ -78,6 +88,32 @@ class FlowsTenSteps
|
|
78
88
|
def s10(**); ok(data: :ok); end
|
79
89
|
end
|
80
90
|
|
91
|
+
class FlowsRailwayTenSteps
|
92
|
+
include Flows::Railway
|
93
|
+
|
94
|
+
step :s1
|
95
|
+
step :s2
|
96
|
+
step :s3
|
97
|
+
step :s4
|
98
|
+
step :s5
|
99
|
+
step :s6
|
100
|
+
step :s7
|
101
|
+
step :s8
|
102
|
+
step :s9
|
103
|
+
step :s10
|
104
|
+
|
105
|
+
def s1(**); ok(s1: true); end
|
106
|
+
def s2(s1:); ok(s2: s1); end
|
107
|
+
def s3(s2:); ok(s3: s2); end
|
108
|
+
def s4(s3:); ok(s4: s3); end
|
109
|
+
def s5(s4:); ok(s5: s4); end
|
110
|
+
def s6(s5:); ok(s6: s5); end
|
111
|
+
def s7(s6:); ok(s7: s6); end
|
112
|
+
def s8(s7:); ok(s8: s7); end
|
113
|
+
def s9(s8:); ok(s9: s8); end
|
114
|
+
def s10(s9:); ok(data: :ok); end
|
115
|
+
end
|
116
|
+
|
81
117
|
class POROTenSteps
|
82
118
|
class << self
|
83
119
|
def call()
|
data/bin/profile_10steps
CHANGED
@@ -9,6 +9,7 @@ require 'stackprof'
|
|
9
9
|
require_relative './examples'
|
10
10
|
|
11
11
|
flows_ten_steps = FlowsTenSteps.new
|
12
|
+
flows_railway_ten_steps = FlowsRailwayTenSteps.new
|
12
13
|
|
13
14
|
build_output_name = '10steps_build_10k_times'
|
14
15
|
exec_output_name = '10steps_execution_10k_times'
|
@@ -18,47 +19,88 @@ exec_output_name = '10steps_execution_10k_times'
|
|
18
19
|
#
|
19
20
|
RubyProf.measure_mode = RubyProf::WALL_TIME
|
20
21
|
|
22
|
+
|
21
23
|
puts 'Build with RubyProf...'
|
24
|
+
|
22
25
|
result = RubyProf.profile do
|
23
26
|
10_000.times do
|
24
27
|
FlowsTenSteps.new
|
25
28
|
end
|
26
29
|
end
|
27
30
|
printer = RubyProf::MultiPrinter.new(result)
|
28
|
-
printer.print(path: 'profile', profile: build_output_name)
|
31
|
+
printer.print(path: 'profile', profile: "#{build_output_name}_operaion")
|
32
|
+
|
33
|
+
result = RubyProf.profile do
|
34
|
+
10_000.times do
|
35
|
+
FlowsRailwayTenSteps.new
|
36
|
+
end
|
37
|
+
end
|
38
|
+
printer = RubyProf::MultiPrinter.new(result)
|
39
|
+
printer.print(path: 'profile', profile: "#{build_output_name}_railway")
|
40
|
+
|
29
41
|
|
30
42
|
puts 'Execution with RubyProf...'
|
43
|
+
|
31
44
|
result = RubyProf.profile do
|
32
45
|
10_000.times {
|
33
46
|
flows_ten_steps.call
|
34
47
|
}
|
35
48
|
end
|
36
49
|
printer = RubyProf::MultiPrinter.new(result)
|
37
|
-
printer.print(path: 'profile', profile: exec_output_name)
|
50
|
+
printer.print(path: 'profile', profile: "#{exec_output_name}_operation")
|
51
|
+
|
52
|
+
result = RubyProf.profile do
|
53
|
+
10_000.times {
|
54
|
+
flows_railway_ten_steps.call
|
55
|
+
}
|
56
|
+
end
|
57
|
+
printer = RubyProf::MultiPrinter.new(result)
|
58
|
+
printer.print(path: 'profile', profile: "#{exec_output_name}_railway")
|
59
|
+
|
38
60
|
|
39
61
|
#
|
40
62
|
# StackProf
|
41
63
|
#
|
42
64
|
|
43
65
|
puts 'Build with StackProf...'
|
66
|
+
|
44
67
|
result = StackProf.run(mode: :wall, raw: true) do
|
45
68
|
10_000.times do
|
46
69
|
FlowsTenSteps.new
|
47
70
|
end
|
48
71
|
end
|
49
|
-
File.write("profile/#{build_output_name}.json", JSON.generate(result))
|
72
|
+
File.write("profile/#{build_output_name}_operation.json", JSON.generate(result))
|
73
|
+
|
74
|
+
result = StackProf.run(mode: :wall, raw: true) do
|
75
|
+
10_000.times do
|
76
|
+
FlowsRailwayTenSteps.new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
File.write("profile/#{build_output_name}_railway.json", JSON.generate(result))
|
80
|
+
|
50
81
|
|
51
82
|
puts 'Execution with StackProf...'
|
83
|
+
|
52
84
|
result = StackProf.run(mode: :wall, raw: true) do
|
53
85
|
10_000.times do
|
54
86
|
flows_ten_steps.call
|
55
87
|
end
|
56
88
|
end
|
57
|
-
File.write("profile/#{exec_output_name}.json", JSON.generate(result))
|
89
|
+
File.write("profile/#{exec_output_name}_operation.json", JSON.generate(result))
|
90
|
+
|
91
|
+
result = StackProf.run(mode: :wall, raw: true) do
|
92
|
+
10_000.times do
|
93
|
+
flows_railway_ten_steps.call
|
94
|
+
end
|
95
|
+
end
|
96
|
+
File.write("profile/#{exec_output_name}_railway.json", JSON.generate(result))
|
97
|
+
|
58
98
|
|
59
99
|
puts
|
60
100
|
puts 'Install speedscope:'
|
61
101
|
puts ' npm i -g speedscope'
|
62
102
|
puts
|
63
|
-
puts "speedscope profile/#{build_output_name}.json"
|
64
|
-
puts "speedscope profile/#{
|
103
|
+
puts "speedscope profile/#{build_output_name}_operation.json"
|
104
|
+
puts "speedscope profile/#{build_output_name}_railway.json"
|
105
|
+
puts "speedscope profile/#{exec_output_name}_operation.json"
|
106
|
+
puts "speedscope profile/#{exec_output_name}_railway.json"
|
data/docs/.nojekyll
ADDED
File without changes
|
data/docs/CNAME
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
flows.ffloyd.tech
|
data/docs/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# Flows
|
2
|
+
|
3
|
+
[](https://github.com/ffloyd/flows/actions)
|
4
|
+
[](https://codecov.io/gh/ffloyd/flows)
|
5
|
+
[](https://badge.fury.io/rb/flows)
|
6
|
+
|
7
|
+
Small and fast ruby framework for implementing railway-like operations.
|
8
|
+
By design it is close to [Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/) and [Dry::Transaction](https://dry-rb.org/gems/dry-transaction/),
|
9
|
+
but has simpler and flexible DSLs for defining operations and matching results. Also `flows` is faster, see [Performance](overview/performance.md).
|
10
|
+
|
11
|
+
`flows` has no production dependencies so it can be used with any framework and cannot bring dependency incompatibilities.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'flows'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
```sh
|
24
|
+
bundle
|
25
|
+
```
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
```sh
|
30
|
+
gem install flows
|
31
|
+
```
|
32
|
+
|
33
|
+
## Flows::Result
|
34
|
+
|
35
|
+
Wrap your data into Result Objects and use convenient matchers for making decisions:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class Example
|
39
|
+
include Flows::Result::Helpers
|
40
|
+
|
41
|
+
def divide(a, b)
|
42
|
+
return err(:zero_division, msg: 'Division by zero is forbidden') if b.zero?
|
43
|
+
|
44
|
+
result = a / b
|
45
|
+
|
46
|
+
if result.negative?
|
47
|
+
ok(:negative, div: result)
|
48
|
+
else
|
49
|
+
ok(:positive, div: result)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def dispatch(result)
|
54
|
+
case result
|
55
|
+
when match_ok(:positive)
|
56
|
+
puts 'Positive result: ' + result.unwrap[:div]
|
57
|
+
when match_ok(:negative)
|
58
|
+
puts 'Negative result: ' + result.unwrap[:div]
|
59
|
+
when match_err
|
60
|
+
raise result.error[:msg]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
example = Example.new
|
66
|
+
|
67
|
+
result = example.divide(4, 2)
|
68
|
+
|
69
|
+
example.dispatch(result) # => Positive result: 2
|
70
|
+
```
|
71
|
+
|
72
|
+
Features:
|
73
|
+
|
74
|
+
* different classes for successful and failure results (`Flows::Result::Ok` and `Flows::Result::Err`)
|
75
|
+
* each result has status (`:positive`, `:negative` and `:zero_division` in the provided example are result statuses)
|
76
|
+
* convenient helpers for creating and matching Result Objects (`#ok`, `#err`, `#math_ok`, `#match_err`)
|
77
|
+
* different data accessor for successful (`#unwrap`) and failure (`#error`) results (prevents using failure objects as successful ones)
|
78
|
+
* Do Notation (like [this one](https://dry-rb.org/gems/dry-monads/1.0/do-notation/) but with a bit [different API](result_objects/do_notation.md))
|
79
|
+
* result has metadata - this may be used for storing execution metadata (execution time, for example, or something for good error reporting)
|
80
|
+
|
81
|
+
More details in a [Result Object Basic Usage Guide](result_objects/basic_usage.md).
|
82
|
+
|
83
|
+
## Flows::Railway
|
84
|
+
|
85
|
+
Organize subsequent data transformations (result of a step becomes input for a next step or a final result):
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class ExampleRailway
|
89
|
+
include Flows::Railway
|
90
|
+
|
91
|
+
step :validate
|
92
|
+
step :add_10
|
93
|
+
step :mul_2
|
94
|
+
|
95
|
+
def validate(x:)
|
96
|
+
return err(:invalid_type, msg: 'Invalid argument type') unless x.is_a?(Numeric)
|
97
|
+
|
98
|
+
ok(x: x)
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_10(x:)
|
102
|
+
ok(x: x + 10)
|
103
|
+
end
|
104
|
+
|
105
|
+
def mul_2(x:)
|
106
|
+
ok(x: x * 2)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
example = ExampleRailway.new
|
111
|
+
|
112
|
+
example.call(x: 2)
|
113
|
+
# => Flows::Result::Ok with data `{x: 24}`
|
114
|
+
|
115
|
+
example.call(x: 'invalid')
|
116
|
+
# => Flows::Result::Err with status `:invalid_type` and data `msg: 'Invalid argument type'`
|
117
|
+
# methods `#add_10` and `#mul_2` not even executed
|
118
|
+
# because Railway stops execution on a first failure result
|
119
|
+
```
|
120
|
+
|
121
|
+
Features:
|
122
|
+
|
123
|
+
* Good composition: `Railway` returns Result Object, step returns Result Object - so you may easily extract steps into separate `Railway`, etc.
|
124
|
+
* Support for inheritance (child class may redefine steps or append new steps to the end of flow)
|
125
|
+
* Less runtime overhead than in `Flows::Operaion`
|
126
|
+
* Override steps implementations using dependency injection on initialization (`.new(deps: {...})`)
|
127
|
+
|
128
|
+
More details in a [Railway Basic Usage Guide](railway/basic_usage.md).
|
129
|
+
|
130
|
+
## Flows::Operation
|
131
|
+
|
132
|
+
If you can draw your business logic in BPMN - you can code it using Operations:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class ExampleOperation
|
136
|
+
include Flows::Operation
|
137
|
+
|
138
|
+
step :fetch_facebook_profile, routes(when_err => :handle_fetch_error)
|
139
|
+
step :fetch_twitter_profile, routes(when_err => :handle_fetch_error)
|
140
|
+
step :extract_person_data
|
141
|
+
|
142
|
+
track :handle_fetch_error do
|
143
|
+
step :track_fetch_error
|
144
|
+
step :make_fetch_error
|
145
|
+
end
|
146
|
+
|
147
|
+
ok_shape :person
|
148
|
+
err_shape :message
|
149
|
+
|
150
|
+
def fetch_facebook_profile(email:, **)
|
151
|
+
result = some_fb_fetcher(email)
|
152
|
+
return err unless result
|
153
|
+
|
154
|
+
ok(facebook_data: result)
|
155
|
+
end
|
156
|
+
|
157
|
+
def fetch_twitter_profile(email:, **)
|
158
|
+
result = some_twitter_fetcher(email)
|
159
|
+
return err unless result
|
160
|
+
|
161
|
+
ok(twitter_data: result)
|
162
|
+
end
|
163
|
+
|
164
|
+
def extract_person_data(facebook_data:, twitter_data:, **)
|
165
|
+
ok(person: facebook_data.merge(twitter_data))
|
166
|
+
end
|
167
|
+
|
168
|
+
def track_fetch_error(**)
|
169
|
+
# send event to New Relic, etc.
|
170
|
+
ok
|
171
|
+
end
|
172
|
+
|
173
|
+
def make_fetch_error(**)
|
174
|
+
err(:fetch_error, message: 'Fetch error')
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
operation = ExampleOperation.new
|
179
|
+
|
180
|
+
operation.call(email: 'whatever@email.com')
|
181
|
+
```
|
182
|
+
|
183
|
+
Features:
|
184
|
+
|
185
|
+
* Superset of `Railway` - any Railway can be converted into Operation in a seconds
|
186
|
+
* Result Shaping - return only data you need
|
187
|
+
* Branching and Tracks - you may do even loops if you brave enough
|
188
|
+
* Good Composition - because everything here returns Result Objects and receives keyword arguments (or hash) you may compose Operations and Railways without any additional effort. Generally speaking - Railway is a simplified operation.
|
189
|
+
|
190
|
+
More details in a [Operation Basic Usage Guide](operation/basic_usage.md).
|
191
|
+
|
192
|
+
## Flows::Flow
|
193
|
+
|
194
|
+
Railway and Operation use `Flows::Flow` under the hood to transform your step definitions into executable workflow.
|
195
|
+
It's not recommended to use Flow in your business code but it's a good tool for building your own abstractions in yours libraries.
|
196
|
+
|
197
|
+
More details [here](flow/general_idea.md).
|