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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +43 -0
  3. data/.mdlrc +1 -0
  4. data/.rubocop.yml +25 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +80 -25
  7. data/README.md +170 -44
  8. data/bin/benchmark +65 -42
  9. data/bin/examples.rb +37 -1
  10. data/bin/profile_10steps +48 -6
  11. data/docs/.nojekyll +0 -0
  12. data/docs/CNAME +1 -0
  13. data/docs/README.md +197 -0
  14. data/docs/_sidebar.md +26 -0
  15. data/docs/contributing/benchmarks_profiling.md +3 -0
  16. data/docs/contributing/local_development.md +3 -0
  17. data/docs/flow/direct_usage.md +3 -0
  18. data/docs/flow/general_idea.md +3 -0
  19. data/docs/index.html +30 -0
  20. data/docs/operation/basic_usage.md +1 -0
  21. data/docs/operation/inject_steps.md +3 -0
  22. data/docs/operation/lambda_steps.md +3 -0
  23. data/docs/operation/result_shapes.md +3 -0
  24. data/docs/operation/routing_tracks.md +3 -0
  25. data/docs/operation/wrapping_steps.md +3 -0
  26. data/docs/overview/performance.md +336 -0
  27. data/docs/railway/basic_usage.md +232 -0
  28. data/docs/result_objects/basic_usage.md +196 -0
  29. data/docs/result_objects/do_notation.md +139 -0
  30. data/flows.gemspec +2 -0
  31. data/forspell.dict +8 -0
  32. data/lefthook.yml +12 -0
  33. data/lib/flows.rb +2 -0
  34. data/lib/flows/flow.rb +1 -1
  35. data/lib/flows/operation.rb +1 -3
  36. data/lib/flows/operation/builder.rb +2 -2
  37. data/lib/flows/operation/dsl.rb +21 -0
  38. data/lib/flows/railway.rb +48 -0
  39. data/lib/flows/railway/builder.rb +68 -0
  40. data/lib/flows/railway/dsl.rb +28 -0
  41. data/lib/flows/railway/errors.rb +21 -0
  42. data/lib/flows/railway/executor.rb +23 -0
  43. data/lib/flows/result.rb +1 -0
  44. data/lib/flows/result/do.rb +30 -0
  45. data/lib/flows/result_router.rb +1 -1
  46. data/lib/flows/version.rb +1 -1
  47. metadata +59 -3
  48. data/.travis.yml +0 -8
@@ -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::Operation (build each time)' do
18
- FlowsSummator.new.call(a: 1, b: 2)
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
- unless ENV['FLOWS_ONLY']
26
- b.report 'Dry::Transaction (build each time)' do
27
- DrySummator.new.call(a: 1, b: 2)
28
- end
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
- b.report 'Dry::Transaction (build once)' do
31
- dry_summator.call(a: 1, b: 2)
32
- end
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
- b.report 'Trailblazer::Operation' do
35
- TBSummator.call(a: 1, b: 2)
36
- end
37
- end
55
+ b.report 'Trailblazer::Operation' do
56
+ TBSummator.call(a: 1, b: 2)
57
+ end if with_trailblazer
38
58
 
39
- if ENV['WITH_PORO']
40
- b.report 'PORO' do
41
- POROSummator.call(a: 1, b: 2)
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 unless ENV['SKIP_SUM']
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::Operation (build each time)' do
59
- FlowsTenSteps.new.call(a: 1, b: 2)
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
- unless ENV['FLOWS_ONLY']
67
- b.report 'Dry::Transaction (build each time)' do
68
- DryTenSteps.new.call(a: 1, b: 2)
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
- b.report 'Dry::Transaction (build once)' do
72
- dry_ten_steps.call(a: 1, b: 2)
73
- end
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
- b.report 'Trailblazer::Operation' do
76
- TBTenSteps.call(a: 1, b: 2)
77
- end
78
- end
101
+ b.report 'Trailblazer::Operation' do
102
+ TBTenSteps.call(a: 1, b: 2)
103
+ end if with_trailblazer
79
104
 
80
- if ENV['WITH_PORO']
81
- b.report 'PORO' do
82
- POROTenSteps.call
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 unless ENV['SKIP_10']
110
+ end
88
111
  puts
@@ -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 returs simple value
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()
@@ -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/#{exec_output_name}.json"
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"
File without changes
@@ -0,0 +1 @@
1
+ flows.ffloyd.tech
@@ -0,0 +1,197 @@
1
+ # Flows
2
+
3
+ [![Build Status](https://github.com/ffloyd/flows/workflows/Build/badge.svg)](https://github.com/ffloyd/flows/actions)
4
+ [![codecov](https://codecov.io/gh/ffloyd/flows/branch/master/graph/badge.svg)](https://codecov.io/gh/ffloyd/flows)
5
+ [![Gem Version](https://badge.fury.io/rb/flows.svg)](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).