flows 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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