flows 0.0.2 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12213313efb3ea2ba0d786043d82ac3ec4279547de96f3b136303eab4078acf9
4
- data.tar.gz: a383ca5ae142ddf2385f0d17e12f4f41f353c4dae577743638b1d2db8748ba05
3
+ metadata.gz: 3a69e7b183fcc1a9a0e0eabeaac0e66aff9fc55ea458fa34a5c37de86c7970ab
4
+ data.tar.gz: 20dbb58536ba20c0f7c7032757679ea968088965890851e93af81e2b4cb148f4
5
5
  SHA512:
6
- metadata.gz: f4ec655af858b58f0ef7da44cbb79d1fc6dfa9d49c985cc5e4fbc504b349a8ca8d68e7b78be73e1b3ec1c7f28f121f7b2456fd7477ef88f5f04aa537bd63c232
7
- data.tar.gz: 8cc7a7e6693c47c2463e8bf2dbf1b6ec4492a3c3125dfd97fbbc8a3e9e1af2763ff6e48320c846cd5a72b44d4c49fae84e14a57b2faba8842154b0793e6591f0
6
+ metadata.gz: b606d7df548e65d694f4700ecf6991667bca8f6626a41111bce6cbe9f8949ad0c5cfc58830fd1596b1fc3c170eaca7aed2b4f8f83092a5a7d174979978ece70b
7
+ data.tar.gz: 4fbfd1e9422aca8b7800b4930d32a7a3a4f4eca67eceab08cf0b20fcde24ef628d677f5bc40ffa1f7a1c9c3af6f1f80de78a6e248adf145f23f63d3d4ae6c77c
data/.gitignore CHANGED
@@ -9,3 +9,7 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ # profile results folder
14
+ /profile/
15
+ !/profile/.keep
data/Gemfile.lock CHANGED
@@ -1,19 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flows (0.0.2)
4
+ flows (0.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.0)
10
+ benchmark-ips (2.7.2)
10
11
  codecov (0.1.14)
11
12
  json
12
13
  simplecov
13
14
  url
14
15
  coderay (1.1.2)
16
+ concurrent-ruby (1.1.5)
15
17
  diff-lcs (1.3)
16
18
  docile (1.3.1)
19
+ dry-configurable (0.8.3)
20
+ concurrent-ruby (~> 1.0)
21
+ dry-core (~> 0.4, >= 0.4.7)
22
+ dry-container (0.7.2)
23
+ concurrent-ruby (~> 1.0)
24
+ dry-configurable (~> 0.1, >= 0.1.3)
25
+ dry-core (0.4.9)
26
+ concurrent-ruby (~> 1.0)
27
+ dry-equalizer (0.2.2)
28
+ dry-events (0.2.0)
29
+ concurrent-ruby (~> 1.0)
30
+ dry-core (~> 0.4)
31
+ dry-equalizer (~> 0.2)
32
+ dry-matcher (0.8.1)
33
+ dry-core (>= 0.4.7)
34
+ dry-monads (1.3.0)
35
+ concurrent-ruby (~> 1.0)
36
+ dry-core (~> 0.4, >= 0.4.4)
37
+ dry-equalizer
38
+ dry-transaction (0.13.0)
39
+ dry-container (>= 0.2.8)
40
+ dry-events (>= 0.1.0)
41
+ dry-matcher (>= 0.7.0)
42
+ dry-monads (>= 0.4.0)
17
43
  jaro_winkler (1.5.2)
18
44
  json (2.2.0)
19
45
  method_source (0.9.2)
@@ -51,12 +77,21 @@ GEM
51
77
  rubocop (>= 0.58.0)
52
78
  rubocop-rspec (1.32.0)
53
79
  rubocop (>= 0.60.0)
80
+ ruby-prof (1.0.0)
54
81
  ruby-progressbar (1.10.0)
55
82
  simplecov (0.16.1)
56
83
  docile (~> 1.1)
57
84
  json (>= 1.8, < 3)
58
85
  simplecov-html (~> 0.10.0)
59
86
  simplecov-html (0.10.2)
87
+ stackprof (0.2.12)
88
+ trailblazer-activity (0.8.4)
89
+ trailblazer-context (>= 0.1.4)
90
+ trailblazer-activity-dsl-linear (0.1.8)
91
+ trailblazer-activity (>= 0.8.3, < 1.0.0)
92
+ trailblazer-context (0.1.4)
93
+ trailblazer-operation (0.5.2)
94
+ trailblazer-activity-dsl-linear (>= 0.1.6, < 1.0.0)
60
95
  unicode-display_width (1.5.0)
61
96
  url (0.3.2)
62
97
 
@@ -64,8 +99,10 @@ PLATFORMS
64
99
  ruby
65
100
 
66
101
  DEPENDENCIES
102
+ benchmark-ips
67
103
  bundler (~> 2.0)
68
104
  codecov
105
+ dry-transaction
69
106
  flows!
70
107
  pry
71
108
  rake (~> 10.0)
@@ -73,7 +110,10 @@ DEPENDENCIES
73
110
  rubocop
74
111
  rubocop-performance
75
112
  rubocop-rspec
113
+ ruby-prof
76
114
  simplecov
115
+ stackprof
116
+ trailblazer-operation
77
117
 
78
118
  BUNDLED WITH
79
119
  2.0.1
data/README.md CHANGED
@@ -4,7 +4,11 @@
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
- _TODO_
7
+ Small and fast ruby framework for implementing railway-like operations.
8
+ By design it close to [Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/) and [Dry::Transaction](https://dry-rb.org/gems/dry-transaction/),
9
+ but has more simpler and flexible DSL for defining operations and matching results. Also `flows` is significantly faster.
10
+
11
+ `flows` has no production dependencies so you can use it with any framework.
8
12
 
9
13
  ## Installation
10
14
 
@@ -24,7 +28,297 @@ Or install it yourself as:
24
28
 
25
29
  ## Usage
26
30
 
27
- _TODO_
31
+ ### `Flows::Flow`
32
+
33
+ Low-level instrument for defining execution flows. Used internally as execution engine for `Flows::Operation`.
34
+ Check out source code and specs for details.
35
+
36
+ ### `Flows::Result`
37
+
38
+ Result Object implementation. Inspired by [Dry::Monads::Result](https://dry-rb.org/gems/dry-monads/1.0/result/) and
39
+ [Rust Result Objects](https://doc.rust-lang.org/1.30.0/book/2018-edition/ch09-02-recoverable-errors-with-result.html).
40
+
41
+ Main concepts & conventions:
42
+
43
+ * separate classes for successful (`Flows::Result::Ok`) and failure (`Flows::Result::Err`) results
44
+ * both classes has same parent class `Flows::Result`
45
+ * result data should be a `Hash` with symbol keys and any values
46
+ * result has a status
47
+ * default status for successful results is `:success`
48
+ * default status for failure results is `:failure`
49
+
50
+ Basic usage:
51
+
52
+ ```ruby
53
+ # create successful result with data {a: 1, b: 2}
54
+ result_ok = Flows::Result::Ok.new(a:1, b: 2)
55
+
56
+ # get `:a` from result
57
+ result_ok.unwrap[:a] # 1
58
+
59
+ # get error data from result
60
+ result_ok.error[:a] # raises exception
61
+
62
+ # get status from result
63
+ result_ok.status # :success
64
+
65
+ # boolean flags
66
+ result_ok.ok? # true
67
+ result_ok.err? # false
68
+
69
+ # create successful result with data {a: 1, b: 2} and status `:custom`
70
+ result_ok_custom = Flows::Result::Ok.new({ a: 1, b: 2 }, status: :custom)
71
+
72
+ # get status from result
73
+ result_ok_custom.status # :custom
74
+
75
+ # create failure result with data {a: 1, b: 2}
76
+ result_err = Flows::Result::Err.new(a:1, b: 2)
77
+
78
+ # get `:a` from result
79
+ result_err.unwrap[:a] # raises exception
80
+
81
+ # get error data from result
82
+ result_err.error[:a] # 1
83
+
84
+ # get status from result
85
+ result_err.status # :failure
86
+
87
+ # boolean flags
88
+ result_ok.ok? # false
89
+ result_ok.err? # true
90
+
91
+ # create failure result with data {a: 1, b: 2} and status `:custom`
92
+ result_err_custom = Flows::Result::Err.new({ a: 1, b: 2 }, status: :custom)
93
+
94
+ # get status from result
95
+ result_err_custom.status # :custom
96
+ ```
97
+
98
+ Mixin `Flows::Result::Helpers` contains tools for simpler generating and matching Result Objects:
99
+
100
+ ```ruby
101
+ include Flows::Result::Helpers
102
+
103
+ # create successful result with data {a: 1, b: 2}
104
+ result_ok = ok(a:1, b: 2)
105
+
106
+ # create successful result with data {a: 1, b: 2} and status `:custom`
107
+ result_ok_custom = ok(:custom, a: 1, b: 2)
108
+
109
+ # create failure result with data {a: 1, b: 2}
110
+ result_err = err(a:1, b: 2)
111
+
112
+ # create failure result with data {a: 1, b: 2} and status `:custom`
113
+ result_err_custom = err(:custom, a: 1, b: 2)
114
+
115
+ # matching helpers
116
+ result = ...
117
+
118
+ case result
119
+ when match_ok(:custom)
120
+ # matches only successful results with status :custom
121
+ when match_ok
122
+ # matches only successful results with any status
123
+ when match_err(:custom)
124
+ # matches only failure results with status :custom
125
+ when match_err
126
+ # matches only failure results with any status
127
+ end
128
+ ```
129
+
130
+ ### `Flows::Operation`
131
+
132
+ Let's solve simple task using operation:
133
+
134
+ * given numbers `a` and `b`
135
+ * result should contain sum of this numbers
136
+ * result should contain square of this sum
137
+
138
+ ```ruby
139
+ class Summator
140
+ # Make this class an operation by including this module.
141
+ # It adds DSL, initializer and call method.
142
+ # Also it includes Flows::Result::Helper both on DSL and instance level.
143
+ include Flows::Operation
144
+
145
+ # This is step definitions.
146
+ # In simplest form step defined by its name and
147
+ # step implementation expected to be in a method
148
+ # with same name.
149
+ #
150
+ # Steps will be executed in a definition order.
151
+ step :validate
152
+ step :calc_sum
153
+ step :calc_square
154
+
155
+ # Which keys of operation data we want to expose on success
156
+ ok_shape :sum, :sum_square
157
+
158
+ # Which keys of operation data we want to expose on failure
159
+ err_shape :message
160
+
161
+ # Step implementation receives execution context as keyword arguments.
162
+ # For the first step context equals to operation arguments.
163
+ #
164
+ # Step implementation must return Result Object.
165
+ # Result Objects's data will be merged into operation context.
166
+ #
167
+ # If result is successful - next step will be executed.
168
+ # If not - operation terminates and returns failure.
169
+ def validate(a:, b:, **)
170
+ err(message: 'a is not a number') if !a.is_a?(Number)
171
+ err(message: 'b is not a number') if !b.is_a?(Number)
172
+
173
+ ok
174
+ end
175
+
176
+ def calc_sum(a:, b:, **)
177
+ ok(sum: a + b)
178
+ end
179
+
180
+ # We may get data from previous steps because all results' data are merged to context.
181
+ def calc_square(sum:, **)
182
+ ok(sum_square: a * b)
183
+ end
184
+ end
185
+
186
+
187
+ # prepare operation
188
+ operation = Summator.new
189
+
190
+ # execute operation
191
+ result = operation.call(a: 1, b: 2)
192
+
193
+ result.ok? # true
194
+ result.unwrap # { sum: 3, sum_square: 9 } - only keys from success shape present
195
+
196
+
197
+ result = operation.call(a: nil, b: nil)
198
+
199
+ result.ok? # false
200
+ result.error # { message: 'a is not a number' } - only keys from error shape present
201
+ ```
202
+
203
+ #### Result Shapes
204
+
205
+ You may limit list of exposed fields by defining success and failure shapes. _After_ step definitions use `ok_shape` to define shapes of success result,
206
+ and `err_shape` to define shapes of failure result. Examples:
207
+
208
+ ```ruby
209
+ # Set exposed keys for :success status of successful result.
210
+ #
211
+ # Success result will have shape like { key1: ..., key2: ... }
212
+ #
213
+ # If one of keys is missing in the final operation context an exception will be raised.
214
+ ok_shape :key1, :key2
215
+
216
+ # Set different exposed keys for different statuses.
217
+ #
218
+ # Operation result status is a status of last executed step result.
219
+ ok_shape status1: [:key1, :key2],
220
+ status2: [:key3]
221
+
222
+ # Failure shapes defined in the same way:
223
+ err_shape :key1, :key2
224
+ err_shape status1: [:key1, :key2],
225
+ status2: [:key3]
226
+ ```
227
+
228
+ Operation definition should have exact one `ok_shape` DSL-call and zero or one `err_shape` DSL-call. If you want to disable shaping
229
+ you can write `no_shape` DSL-call instead of shape definitions.
230
+
231
+ #### Routing & Tracks
232
+
233
+ You define side tracks, even nested ones:
234
+
235
+ ```ruby
236
+ step :outer_1 # next step is outer_2
237
+
238
+ track :some_track do
239
+ step :inner_1 # next step is inner_2
240
+ track :inner_track do
241
+ step :deep_1 # next step is deep_2
242
+ step :deep_2 # next step is inner_2
243
+ end
244
+ step :inner_2 # next step in outer_2
245
+ end
246
+
247
+ step :outer_2
248
+ ```
249
+
250
+ In definition above tracks will not be used because there is no routes to this tracks. You may define routing like this:
251
+
252
+ ```ruby
253
+ # if result is successful and has status :to_some_track - next step will be inner_1
254
+ # for any other successful results - outer_2
255
+ step :outer_1,
256
+ match_ok(:to_some_track) => :some_track
257
+
258
+ track :some_track do
259
+ step :inner_1, match_err => :inner_track # redirect to inner_track on failure result
260
+ track :inner_track do
261
+ step :deep_1, match_ok(:some_status) => :outer_2 # you may redirect to steps too
262
+ step :deep_2
263
+ end
264
+ step :inner_2
265
+ end
266
+
267
+ step :outer_2
268
+ ```
269
+
270
+ #### Lambda Steps
271
+
272
+ You can use lambda for in-place step implementation:
273
+
274
+ ```ruby
275
+ step :name, ->(a:, b:, **) { ok(sum: a + b) }
276
+ ```
277
+
278
+ #### Dependency Injection
279
+
280
+ You can override or inject step implementation on initialization:
281
+
282
+ ```ruby
283
+ class Summator
284
+ include Flows::Operation
285
+
286
+ step :sum
287
+
288
+ ok_shape :sum
289
+ end
290
+
291
+ summator = Summator.new(deps: {
292
+ sum: ->(a:, b:, **) { ok(sum: a + b) }
293
+ })
294
+
295
+ summator.call(a: 1, b: 2).unwrap[:sum] # 3
296
+ ```
297
+
298
+ #### Wrapping steps
299
+
300
+ You can wrap several steps with some logic:
301
+
302
+ ```ruby
303
+ step :first
304
+
305
+ wrap :wrapper do
306
+ step :wrapped
307
+ end
308
+
309
+ def wrapper(**context)
310
+ # do smth
311
+ result = yield # execute wrapped steps
312
+ # do smth or modify result
313
+ result
314
+ end
315
+ ```
316
+
317
+ There is routing limitation when you use wrap:
318
+
319
+ * outside `wrap` block you may route to wrapped block by wrapper name (`:wrapper` in the provided example)
320
+ * you may route wrapped steps only to wrapped steps in the same wrap block
321
+ * you cannot route to wrapped steps from outside
28
322
 
29
323
  ## Development
30
324
 
data/bin/benchmark ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+ # rubocop:disable all
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark/ips'
6
+
7
+ require_relative './examples'
8
+
9
+ puts '-' * 50
10
+ puts '- task: A + B, one step implementation'
11
+ puts '-' * 50
12
+
13
+ flows_summator = FlowsSummator.new
14
+ dry_summator = DrySummator.new
15
+
16
+ Benchmark.ips do |b|
17
+ b.report 'Flows::Operation (build each time)' do
18
+ FlowsSummator.new.call(a: 1, b: 2)
19
+ end
20
+
21
+ b.report 'Flows::Operation (build once)' do
22
+ flows_summator.call(a: 1, b: 2)
23
+ end
24
+
25
+ unless ENV['FLOWS_ONLY']
26
+ b.report 'Dry::Transaction (build each time)' do
27
+ DrySummator.new.call(a: 1, b: 2)
28
+ end
29
+
30
+ b.report 'Dry::Transaction (build once)' do
31
+ dry_summator.call(a: 1, b: 2)
32
+ end
33
+
34
+ b.report 'Trailblazer::Operation' do
35
+ TBSummator.call(a: 1, b: 2)
36
+ end
37
+ end
38
+
39
+ if ENV['WITH_PORO']
40
+ b.report 'PORO' do
41
+ POROSummator.call(a: 1, b: 2)
42
+ end
43
+ end
44
+
45
+ b.compare!
46
+ end unless ENV['SKIP_SUM']
47
+ puts
48
+
49
+
50
+ puts '-' * 50
51
+ puts '- task: ten steps returns successful result'
52
+ puts '-' * 50
53
+
54
+ flows_ten_steps = FlowsTenSteps.new
55
+ dry_ten_steps = DryTenSteps.new
56
+
57
+ Benchmark.ips do |b|
58
+ b.report 'Flows::Operation (build each time)' do
59
+ FlowsTenSteps.new.call(a: 1, b: 2)
60
+ end
61
+
62
+ b.report 'Flows::Operation (build once)' do
63
+ flows_ten_steps.call(a: 1, b: 2)
64
+ end
65
+
66
+ unless ENV['FLOWS_ONLY']
67
+ b.report 'Dry::Transaction (build each time)' do
68
+ DryTenSteps.new.call(a: 1, b: 2)
69
+ end
70
+
71
+ b.report 'Dry::Transaction (build once)' do
72
+ dry_ten_steps.call(a: 1, b: 2)
73
+ end
74
+
75
+ b.report 'Trailblazer::Operation' do
76
+ TBTenSteps.call(a: 1, b: 2)
77
+ end
78
+ end
79
+
80
+ if ENV['WITH_PORO']
81
+ b.report 'PORO' do
82
+ POROTenSteps.call
83
+ end
84
+ end
85
+
86
+ b.compare!
87
+ end unless ENV['SKIP_10']
88
+ puts
data/bin/demo CHANGED
@@ -27,8 +27,8 @@ class DivisionOperation
27
27
  step :check_for_zero
28
28
  step :divide
29
29
 
30
- success :result
31
- failure :error
30
+ ok_shape :result
31
+ err_shape :error
32
32
 
33
33
  def check_for_zero(denominator:, **)
34
34
  if denominator.zero?
@@ -49,8 +49,8 @@ class NestedDivisionOperation
49
49
 
50
50
  step :do_division
51
51
 
52
- success :result
53
- failure :error
52
+ ok_shape :result
53
+ err_shape :error
54
54
 
55
55
  def do_division(**params)
56
56
  DivisionOperation.new.call(**params)
data/bin/examples.rb ADDED
@@ -0,0 +1,159 @@
1
+ # rubocop:disable all
2
+ require 'flows'
3
+ require 'dry/transaction'
4
+ require 'trailblazer/operation'
5
+
6
+ #
7
+ # Task: a + b = ?
8
+ #
9
+
10
+ class FlowsSummator
11
+ include Flows::Operation
12
+
13
+ step :sum
14
+
15
+ ok_shape :sum
16
+
17
+ def sum(a:, b:, **)
18
+ ok(sum: a + b)
19
+ end
20
+ end
21
+
22
+ class POROSummator
23
+ def self.call(a:, b:)
24
+ a + b
25
+ end
26
+ end
27
+
28
+ class DrySummator
29
+ include Dry::Transaction
30
+
31
+ step :sum
32
+
33
+ private
34
+
35
+ def sum(a:, b:)
36
+ Success(a + b)
37
+ end
38
+ end
39
+
40
+ class TBSummator < Trailblazer::Operation
41
+ step :sum
42
+
43
+ def sum(opts, a:, b:, **)
44
+ opts[:sum] = a + b
45
+ end
46
+ end
47
+
48
+ #
49
+ # Task: 10 steps which returs simple value
50
+ #
51
+
52
+ class FlowsTenSteps
53
+ include Flows::Operation
54
+
55
+ step :s1
56
+ step :s2
57
+ step :s3
58
+ step :s4
59
+ step :s5
60
+ step :s6
61
+ step :s7
62
+ step :s8
63
+ step :s9
64
+ step :s10
65
+
66
+ ok_shape :data
67
+
68
+ def s1(**); ok(s1: true); end
69
+ def s2(**); ok(s2: true); end
70
+ def s3(**); ok(s3: true); end
71
+ def s4(**); ok(s4: true); end
72
+ def s5(**); ok(s5: true); end
73
+ def s5(**); ok(s5: true); end
74
+ def s6(**); ok(s6: true); end
75
+ def s7(**); ok(s7: true); end
76
+ def s8(**); ok(s8: true); end
77
+ def s9(**); ok(s9: true); end
78
+ def s10(**); ok(data: :ok); end
79
+ end
80
+
81
+ class POROTenSteps
82
+ class << self
83
+ def call()
84
+ s1
85
+ s2
86
+ s3
87
+ s4
88
+ s5
89
+ s6
90
+ s7
91
+ s8
92
+ s9
93
+ s10
94
+ end
95
+
96
+ def s1; true; end
97
+ def s2; true; end
98
+ def s3; true; end
99
+ def s4; true; end
100
+ def s5; true; end
101
+ def s6; true; end
102
+ def s7; true; end
103
+ def s8; true; end
104
+ def s9; true; end
105
+ def s10; true; end
106
+ end
107
+ end
108
+
109
+ class DryTenSteps
110
+ include Dry::Transaction
111
+
112
+ step :s1
113
+ step :s2
114
+ step :s3
115
+ step :s4
116
+ step :s5
117
+ step :s6
118
+ step :s7
119
+ step :s8
120
+ step :s9
121
+ step :s10
122
+
123
+ private
124
+
125
+ def s1; Success(true); end
126
+ def s2; Success(true); end
127
+ def s3; Success(true); end
128
+ def s4; Success(true); end
129
+ def s5; Success(true); end
130
+ def s6; Success(true); end
131
+ def s7; Success(true); end
132
+ def s8; Success(true); end
133
+ def s9; Success(true); end
134
+ def s10; Success(true); end
135
+ end
136
+
137
+ class TBTenSteps < Trailblazer::Operation
138
+ step :s1
139
+ step :s2
140
+ step :s3
141
+ step :s4
142
+ step :s5
143
+ step :s6
144
+ step :s7
145
+ step :s8
146
+ step :s9
147
+ step :s10
148
+
149
+ def s1(opts, **); opts[:s1] = true; end
150
+ def s2(opts, **); opts[:s2] = true; end
151
+ def s3(opts, **); opts[:s3] = true; end
152
+ def s4(opts, **); opts[:s4] = true; end
153
+ def s5(opts, **); opts[:s5] = true; end
154
+ def s6(opts, **); opts[:s6] = true; end
155
+ def s7(opts, **); opts[:s7] = true; end
156
+ def s8(opts, **); opts[:s8] = true; end
157
+ def s9(opts, **); opts[:s9] = true; end
158
+ def s10(opts, **); opts[:s10] = true; end
159
+ end