flow 0.10.4 → 0.10.5
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/README.md +140 -8
- data/lib/flow.rb +13 -1
- data/lib/flow/flow/callbacks.rb +7 -5
- data/lib/flow/flow/core.rb +16 -14
- data/lib/flow/flow/flux.rb +35 -33
- data/lib/flow/flow/operations.rb +16 -14
- data/lib/flow/flow/status.rb +15 -13
- data/lib/flow/flow/transactions.rb +7 -5
- data/lib/flow/flow/trigger.rb +26 -24
- data/lib/flow/flow_base.rb +2 -6
- data/lib/flow/operation/core.rb +12 -1
- data/lib/flow/operation/execute.rb +1 -1
- data/lib/flow/operation_base.rb +10 -15
- data/lib/flow/{spec_helper/rspec_configuration.rb → rspec/configuration.rb} +0 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers.rb +3 -0
- data/lib/flow/rspec/custom_matchers/access_state.rb +27 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/define_failure.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/define_output.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/handle_error.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/have_on_state.rb +0 -0
- data/lib/flow/rspec/custom_matchers/read_state.rb +23 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/use_operations.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/custom_matchers/wrap_in_transaction.rb +0 -0
- data/lib/flow/rspec/custom_matchers/write_state.rb +23 -0
- data/lib/flow/{spec_helper → rspec}/shared_contexts.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/shared_contexts/with_failing_operation.rb +0 -0
- data/lib/flow/{spec_helper → rspec}/shoulda_matcher_helper.rb +0 -0
- data/lib/flow/spec_helper.rb +4 -4
- data/lib/flow/state/output.rb +1 -1
- data/lib/flow/state_base.rb +2 -4
- data/lib/flow/state_proxy.rb +30 -0
- data/lib/flow/version.rb +1 -1
- data/lib/generators/flow/operation/templates/operation.rb.erb +4 -0
- data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +9 -5
- data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +4 -0
- metadata +33 -18
- data/lib/flow/flow/errors/state_invalid.rb +0 -7
- data/lib/flow/operation/errors/already_executed.rb +0 -9
- data/lib/flow/operation/errors/already_rewound.rb +0 -9
- data/lib/flow/state/errors/not_validated.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 946ff11930523a9982a5f4210d74ef656e66c0f84c0267f484799b6a4173956e
|
4
|
+
data.tar.gz: 7c148b9935abaf957f966c3f576620f5d444e5d8561e35fa40cbc3dc7c525ca7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3c5c2cf05a2b9ffbe25bf0ebad9a3b33c83d3f76a79f76717e7ca96744a39b89778cb8fb4ad3cb6522c1f59094fd9619cc878ec4152a26c6876c01e6afadbcb
|
7
|
+
data.tar.gz: 374fdd652be6bc5bff0db2f5eed97f974f0e78cd0512742294ad8196fa47e58fa72b21ca319375314c0d98ffedc01b1d11279d9a5645b488bef4151d89d17c44
|
data/README.md
CHANGED
@@ -11,6 +11,7 @@
|
|
11
11
|
* [How it Works](#how-it-works)
|
12
12
|
* [Flows](#flows)
|
13
13
|
* [Operations](#operations)
|
14
|
+
* [Accessors](#accessors)
|
14
15
|
* [States](#states)
|
15
16
|
* [Input](#input)
|
16
17
|
* [Output](#output)
|
@@ -146,19 +147,24 @@ An **Operation** is a service object which is executed with a **State**.
|
|
146
147
|
Operations should **not** be named with the `Operation` suffix; name them what they do!
|
147
148
|
|
148
149
|
```ruby
|
149
|
-
class
|
150
|
+
class ClearExistingTimetable < ApplicationOperation
|
151
|
+
state_accessor :existing_timetable_cells
|
152
|
+
|
150
153
|
def behavior
|
151
|
-
|
154
|
+
existing_timetable_cells.update_all(total_minutes: 0)
|
152
155
|
end
|
153
156
|
end
|
154
157
|
```
|
155
158
|
|
156
159
|
```ruby
|
157
160
|
class CalculateTimetables < ApplicationOperation
|
161
|
+
state_accessor :minutes_by_project_employee
|
162
|
+
state_reader :timeframe
|
163
|
+
|
158
164
|
def behavior
|
159
|
-
|
165
|
+
minutes_by_project_employee.each do |project_employee, total_minutes|
|
160
166
|
project_id, employee_id = project_employee
|
161
|
-
timetable =
|
167
|
+
timetable = timeframe.timetables.find_or_create_by!(project_id: project_id)
|
162
168
|
cell = timetable.cells.find_or_create_by!(employee_id: employee_id)
|
163
169
|
|
164
170
|
cell.update!(total_minutes: total_minutes)
|
@@ -169,8 +175,10 @@ end
|
|
169
175
|
|
170
176
|
```ruby
|
171
177
|
class SummarizeTimetables < ApplicationOperation
|
178
|
+
state_accessor :timetables
|
179
|
+
|
172
180
|
def behavior
|
173
|
-
|
181
|
+
timetables.each do |timetable|
|
174
182
|
timetable.update!(total_minutes: timetable.cells.sum(:total_minutes))
|
175
183
|
end
|
176
184
|
end
|
@@ -179,8 +187,10 @@ end
|
|
179
187
|
|
180
188
|
```ruby
|
181
189
|
class DestroyEmptyTimetableCells < ApplicationOperation
|
190
|
+
state_accessor :empty_cells
|
191
|
+
|
182
192
|
def behavior
|
183
|
-
|
193
|
+
empty_cells.destroy_all
|
184
194
|
end
|
185
195
|
end
|
186
196
|
```
|
@@ -191,8 +201,10 @@ Operations take a state as input and define a `#behavior` that occurs when `#exe
|
|
191
201
|
|
192
202
|
```ruby
|
193
203
|
class ExampleOperation < ApplicationOperation
|
204
|
+
state_reader :first_name
|
205
|
+
|
194
206
|
def behavior
|
195
|
-
puts "Hello, #{
|
207
|
+
puts "Hello, #{first_name}"
|
196
208
|
end
|
197
209
|
end
|
198
210
|
|
@@ -202,6 +214,117 @@ operation.execute
|
|
202
214
|
operation.executed? # => true
|
203
215
|
```
|
204
216
|
|
217
|
+
⚠️ **Deprecation Warning**: Direct state access in Operations is being removed. [Learn more](./DEPRECATION_NOTICE.md)
|
218
|
+
|
219
|
+
#### Accessors
|
220
|
+
|
221
|
+
**Operations** define, through *State Accessors*, what data they will read from and/or write to a **State**.
|
222
|
+
|
223
|
+
These accessors provide an explicit means to declare input needs and output expectations.
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class ExampleOperation < ApplicationOperation
|
227
|
+
state_reader :foo
|
228
|
+
state_writer :bar
|
229
|
+
state_accessor :baz
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
Under the hood, a **StateProxy** adapts a **State** to an **Operation** using these accessors.
|
234
|
+
|
235
|
+
⚠️ *Warning*: Your Operation will not be able to access methods on your State if you do not declare them using these accessors!
|
236
|
+
|
237
|
+
The following are considered best practices for working with state accessors:
|
238
|
+
|
239
|
+
`state_reader` should **only** read data and not alter it
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# Bad, don't do it this way:
|
243
|
+
class BadOperation < ApplicationOperation
|
244
|
+
state_reader :foo
|
245
|
+
|
246
|
+
def behavior
|
247
|
+
foo << :more_dataz
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Good, do it this way:
|
252
|
+
class GoodOperation < ApplicationOperation
|
253
|
+
state_reader :foo
|
254
|
+
|
255
|
+
def behavior
|
256
|
+
SomeThirdParty.do_a_thing if foo == :foo
|
257
|
+
end
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
`state_writer` should map to a field marked as `output` on the State
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class ExampleOperation < ApplicationOperation
|
265
|
+
state_writer :foo
|
266
|
+
|
267
|
+
def behavior
|
268
|
+
state.foo = :foo
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Bad, don't do it this way:
|
273
|
+
class BadState < ApplicationState
|
274
|
+
option :foo
|
275
|
+
end
|
276
|
+
|
277
|
+
# Good, do it this way:
|
278
|
+
class GoodState < ApplicationState
|
279
|
+
output :foo
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
`state_accessor` should be used rather than defining both setters and getters:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
# Bad, don't do it this way:
|
287
|
+
class BadOperation < ApplicationOperation
|
288
|
+
state_reader :foo
|
289
|
+
state_writer :foo
|
290
|
+
|
291
|
+
def behavior
|
292
|
+
state.foo = :bar if foo == :baz
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Good, do it this way:
|
297
|
+
class GoodOperation < ApplicationOperation
|
298
|
+
state_accessor :foo
|
299
|
+
|
300
|
+
def behavior
|
301
|
+
state.foo = :bar if foo == :baz
|
302
|
+
end
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
`state_accessor` should also be used when altering a memory object (ex: Array, Hash, ActiveModel):
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
# Bad, don't do it this way:
|
310
|
+
class BadOperation < ApplicationOperation
|
311
|
+
state_reader :foo
|
312
|
+
|
313
|
+
def behavior
|
314
|
+
foo << :more_dataz
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Good, do it this way:
|
319
|
+
class GoodOperation < ApplicationOperation
|
320
|
+
state_accessor :foo
|
321
|
+
|
322
|
+
def behavior
|
323
|
+
foo << :more_dataz
|
324
|
+
end
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
205
328
|
### States
|
206
329
|
|
207
330
|
A **State** is an aggregation of input and derived data.
|
@@ -1024,6 +1147,9 @@ This will allow you to use the following custom matchers:
|
|
1024
1147
|
* [use_operations](lib/flow/custom_matchers/use_operations.rb) tests usage of `ApplicationFlow.operations`
|
1025
1148
|
* [wrap_in_transaction](lib/flow/custom_matchers/wrap_in_transaction.rb) tests usage of `.wrap_in_transaction` for `ApplicationFlow` or `ApplicationOperation`
|
1026
1149
|
* [have_on_state](lib/flow/custom_matchers/have_on_state.rb) tests for data on State after a `ApplicationFlow` or `ApplicationOperation` has been run
|
1150
|
+
* [access_state](lib/flow/custom_matchers/access_state.rb) tests usage of `ApplicationOperation.state_accessor` (or use of both `state_(reader|writer)`)
|
1151
|
+
* [read_state](lib/flow/custom_matchers/read_state.rb) tests usage of `ApplicationOperation.state_reader`
|
1152
|
+
* [write_state](lib/flow/custom_matchers/write_state.rb) tests usage of `ApplicationOperation.state_writer`
|
1027
1153
|
|
1028
1154
|
### Testing Flows
|
1029
1155
|
|
@@ -1086,6 +1212,10 @@ RSpec.describe MakeTheThingDoTheStuff, type: :operation do
|
|
1086
1212
|
|
1087
1213
|
it { is_expected.to inherit_from ApplicationOperation }
|
1088
1214
|
|
1215
|
+
# it { is_expected.to access_state :foo }
|
1216
|
+
# it { is_expected.to read_state :bar }
|
1217
|
+
# it { is_expected.to write_state :baz }
|
1218
|
+
|
1089
1219
|
describe "#execute" do
|
1090
1220
|
subject(:execute) { operation.execute }
|
1091
1221
|
|
@@ -1145,10 +1275,12 @@ You are encouraged to use `execute`, rather than `execute!` in testing. You can
|
|
1145
1275
|
Doing so will allow you to make assertions on the failure without having to expect errors:
|
1146
1276
|
```ruby
|
1147
1277
|
class SomeOperation < ApplicationOperation
|
1278
|
+
state_reader :foo
|
1279
|
+
|
1148
1280
|
failure :somethings_invalid
|
1149
1281
|
|
1150
1282
|
def behavior
|
1151
|
-
somethings_invalid_failure! baz: "relevant data" if
|
1283
|
+
somethings_invalid_failure! baz: "relevant data" if foo == "something invalid"
|
1152
1284
|
end
|
1153
1285
|
end
|
1154
1286
|
```
|
data/lib/flow.rb
CHANGED
@@ -13,5 +13,17 @@ require "flow/concerns/transaction_wrapper"
|
|
13
13
|
require "flow/flow_base"
|
14
14
|
require "flow/operation_base"
|
15
15
|
require "flow/state_base"
|
16
|
+
require "flow/state_proxy"
|
16
17
|
|
17
|
-
module Flow
|
18
|
+
module Flow
|
19
|
+
class Error < StandardError; end
|
20
|
+
|
21
|
+
class FlowError < Error; end
|
22
|
+
class StateInvalidError < FlowError; end
|
23
|
+
|
24
|
+
class OperationError < Error; end
|
25
|
+
class AlreadyExecutedError < OperationError; end
|
26
|
+
|
27
|
+
class StateError < Error; end
|
28
|
+
class NotValidatedError < StateError; end
|
29
|
+
end
|
data/lib/flow/flow/callbacks.rb
CHANGED
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
# Callbacks provide an extensible mechanism for hooking into a Flow.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
included do
|
10
|
+
include ActiveSupport::Callbacks
|
11
|
+
define_callbacks :initialize, :trigger, :flux
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
data/lib/flow/flow/core.rb
CHANGED
@@ -2,25 +2,27 @@
|
|
2
2
|
|
3
3
|
# Accepts input representing the arguments and options which define the initial state.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Core
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
class_methods do
|
10
|
+
def state_class
|
11
|
+
"#{name.chomp("Flow")}State".constantize
|
12
|
+
end
|
11
13
|
end
|
12
|
-
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
included do
|
16
|
+
delegate :state_class, to: :class
|
17
|
+
delegate :outputs, to: :state
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
attr_reader :state
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
def initialize(state_instance = nil, **options)
|
23
|
+
run_callbacks(:initialize) do
|
24
|
+
@state = state_instance || state_class.new(**options)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/flow/flow/flux.rb
CHANGED
@@ -2,54 +2,56 @@
|
|
2
2
|
|
3
3
|
# When `#trigger` is called on a Flow, `#execute` is called on Operations sequentially in their given order.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Flux
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
included do
|
10
|
+
set_callback(:initialize, :after) { @executed_operations = [] }
|
10
11
|
|
11
|
-
|
12
|
+
attr_reader :failed_operation
|
12
13
|
|
13
|
-
|
14
|
+
delegate :operation_failure, to: :failed_operation, allow_nil: true
|
14
15
|
|
15
|
-
|
16
|
+
private
|
16
17
|
|
17
|
-
|
18
|
+
attr_reader :executed_operations
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
def _flux
|
21
|
+
executable_operations.each do |operation|
|
22
|
+
operation.execute
|
23
|
+
(@failed_operation = operation) and raise Flow::Flux::Failure if operation.failed?
|
24
|
+
executed_operations << operation
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def executable_operations
|
29
|
+
operation_instances - executed_operations
|
24
30
|
end
|
25
|
-
end
|
26
31
|
|
27
|
-
|
28
|
-
|
32
|
+
def operation_instances
|
33
|
+
_operations.map { |operation_class| operation_class.new(state) }
|
34
|
+
end
|
35
|
+
memoize :operation_instances
|
29
36
|
end
|
30
37
|
|
31
|
-
def
|
32
|
-
|
38
|
+
def failed_operation?
|
39
|
+
failed_operation.present?
|
33
40
|
end
|
34
|
-
memoize :operation_instances
|
35
|
-
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
42
|
+
def flux
|
43
|
+
flux!
|
44
|
+
rescue StandardError => exception
|
45
|
+
info :error_executing_operation, state: state, exception: exception
|
40
46
|
|
41
|
-
|
42
|
-
|
43
|
-
rescue StandardError => exception
|
44
|
-
info :error_executing_operation, state: state, exception: exception
|
47
|
+
raise exception unless exception.is_a? Flow::Flux::Failure
|
48
|
+
end
|
45
49
|
|
46
|
-
|
47
|
-
|
50
|
+
def flux!
|
51
|
+
run_callbacks(:flux) { _flux }
|
52
|
+
end
|
48
53
|
|
49
|
-
|
50
|
-
run_callbacks(:flux) { _flux }
|
54
|
+
class Failure < StandardError; end
|
51
55
|
end
|
52
|
-
|
53
|
-
class Failure < StandardError; end
|
54
56
|
end
|
55
57
|
end
|
data/lib/flow/flow/operations.rb
CHANGED
@@ -2,25 +2,27 @@
|
|
2
2
|
|
3
3
|
# Operations are an ordered list of the behaviors which are executed with (and possibly change) the Flow's state.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Operations
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
included do
|
10
|
+
class_attribute :_operations, instance_writer: false, default: []
|
10
11
|
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
class_methods do
|
15
|
-
def inherited(base)
|
16
|
-
base._operations = _operations.dup
|
17
|
-
super
|
12
|
+
delegate :_operations, to: :class
|
18
13
|
end
|
19
14
|
|
20
|
-
|
15
|
+
class_methods do
|
16
|
+
def inherited(base)
|
17
|
+
base._operations = _operations.dup
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
def operations(*operations)
|
24
|
+
_operations.concat(operations.flatten)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/flow/flow/status.rb
CHANGED
@@ -2,23 +2,25 @@
|
|
2
2
|
|
3
3
|
# The Flow status is a summary of what has occurred during the runtime, used mainly for analysis and program flow.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Status
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def pending?
|
10
|
+
executed_operations.none?
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def triggered?
|
14
|
+
executed_operations.any? || failed_operation?
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def success?
|
18
|
+
triggered? && (operation_instances - executed_operations).none?
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
def failed?
|
22
|
+
triggered? && !success?
|
23
|
+
end
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
# Flows where no operation should be persisted unless all are successful should use a transaction.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
5
|
+
module Flow
|
6
|
+
module Transactions
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
class_methods do
|
10
|
+
def callback_name
|
11
|
+
:flux
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
data/lib/flow/flow/trigger.rb
CHANGED
@@ -2,37 +2,39 @@
|
|
2
2
|
|
3
3
|
# Triggering a Flow executes all its operations in sequential order (see `Flow::Flux`) *iff* it has a valid state.
|
4
4
|
module Flow
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
module Flow
|
6
|
+
module Trigger
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def trigger!(*arguments)
|
11
|
+
new(*arguments).trigger!
|
12
|
+
end
|
13
|
+
|
14
|
+
def trigger(*arguments)
|
15
|
+
new(*arguments).trigger
|
16
|
+
end
|
11
17
|
end
|
12
18
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
19
|
+
included do
|
20
|
+
delegate :valid?, :validated?, to: :state, prefix: true
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
|
22
|
-
end
|
22
|
+
set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
def trigger!
|
26
|
+
raise StateInvalidError unless state_valid?
|
26
27
|
|
27
|
-
|
28
|
+
run_callbacks(:trigger) { flux }
|
28
29
|
|
29
|
-
|
30
|
-
|
30
|
+
self
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def trigger
|
34
|
+
trigger!
|
35
|
+
rescue StateInvalidError
|
36
|
+
self
|
37
|
+
end
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
data/lib/flow/flow_base.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "flow/errors/state_invalid"
|
4
|
-
|
5
3
|
require_relative "flow/callbacks"
|
6
4
|
require_relative "flow/core"
|
7
5
|
require_relative "flow/flux"
|
@@ -12,10 +10,8 @@ require_relative "flow/trigger"
|
|
12
10
|
|
13
11
|
# A **Flow** is a collection of procedurally executed **Operations** sharing a common **State**.
|
14
12
|
module Flow
|
15
|
-
class FlowBase
|
16
|
-
include
|
17
|
-
include Technologic
|
18
|
-
include Flow::TransactionWrapper
|
13
|
+
class FlowBase < Spicerack::RootObject
|
14
|
+
include TransactionWrapper
|
19
15
|
include Flow::Callbacks
|
20
16
|
include Flow::Core
|
21
17
|
include Flow::Flux
|
data/lib/flow/operation/core.rb
CHANGED
@@ -8,10 +8,21 @@ module Flow
|
|
8
8
|
|
9
9
|
included do
|
10
10
|
attr_reader :state
|
11
|
+
|
12
|
+
delegate :state_proxy_class, to: :class
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def state_proxy_class
|
17
|
+
@state_proxy_class ||= Class.new(StateProxy).tap do |proxy_class|
|
18
|
+
delegate_method_names = _state_writers.map { |method_name| "#{method_name}=" } + _state_readers
|
19
|
+
proxy_class.delegate(*delegate_method_names, to: :state) if delegate_method_names.any?
|
20
|
+
end
|
21
|
+
end
|
11
22
|
end
|
12
23
|
|
13
24
|
def initialize(state)
|
14
|
-
@state = state
|
25
|
+
@state = state_proxy_class.new(state)
|
15
26
|
end
|
16
27
|
end
|
17
28
|
end
|
@@ -12,7 +12,7 @@ module Flow
|
|
12
12
|
attr_reader :operation_failure
|
13
13
|
|
14
14
|
set_callback :execute, :around, ->(_, block) { surveil(:execute) { block.call } }
|
15
|
-
set_callback :execute, :before, -> { raise
|
15
|
+
set_callback :execute, :before, -> { raise AlreadyExecutedError }, if: :executed?
|
16
16
|
end
|
17
17
|
|
18
18
|
def execute!
|
data/lib/flow/operation_base.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "operation/errors/already_executed"
|
4
|
-
require_relative "operation/errors/already_rewound"
|
5
|
-
|
6
3
|
require_relative "operation/accessors"
|
7
4
|
require_relative "operation/callbacks"
|
8
5
|
require_relative "operation/core"
|
@@ -14,17 +11,15 @@ require_relative "operation/transactions"
|
|
14
11
|
|
15
12
|
# An **Operation** is a service object which is executed with a **State**.
|
16
13
|
module Flow
|
17
|
-
class OperationBase
|
18
|
-
include
|
19
|
-
include
|
20
|
-
include
|
21
|
-
include
|
22
|
-
include
|
23
|
-
include
|
24
|
-
include
|
25
|
-
include
|
26
|
-
include
|
27
|
-
include Flow::Operation::Status
|
28
|
-
include Flow::Operation::Transactions
|
14
|
+
class OperationBase < Spicerack::RootObject
|
15
|
+
include TransactionWrapper
|
16
|
+
include Operation::Accessors
|
17
|
+
include Operation::Callbacks
|
18
|
+
include Operation::Core
|
19
|
+
include Operation::ErrorHandler
|
20
|
+
include Operation::Execute
|
21
|
+
include Operation::Failures
|
22
|
+
include Operation::Status
|
23
|
+
include Operation::Transactions
|
29
24
|
end
|
30
25
|
end
|
File without changes
|
@@ -6,3 +6,6 @@ require_relative "custom_matchers/handle_error"
|
|
6
6
|
require_relative "custom_matchers/use_operations"
|
7
7
|
require_relative "custom_matchers/wrap_in_transaction"
|
8
8
|
require_relative "custom_matchers/have_on_state"
|
9
|
+
require_relative "custom_matchers/read_state"
|
10
|
+
require_relative "custom_matchers/write_state"
|
11
|
+
require_relative "custom_matchers/access_state"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `ApplicationOperation.state_accessor` (or use of both `state_(reader|writer)`)
|
4
|
+
#
|
5
|
+
# class SomeTask < ApplicationOperation
|
6
|
+
# state_accessor :foo
|
7
|
+
# state_accessor :bar
|
8
|
+
#
|
9
|
+
# state_reader :baz
|
10
|
+
# state_writer :baz
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# RSpec.describe SomeTask, type: :operation do
|
14
|
+
# subject { described_class.new(state) }
|
15
|
+
#
|
16
|
+
# let(:state) { Class.new(ApplicationState).new }
|
17
|
+
#
|
18
|
+
# it { is_expected.to access_state :foo }
|
19
|
+
# it { is_expected.to access_state :bar }
|
20
|
+
# it { is_expected.to access_state :baz }
|
21
|
+
# end
|
22
|
+
|
23
|
+
RSpec::Matchers.define :access_state do |field|
|
24
|
+
match { |operation| expect(operation._state_accessors).to include field }
|
25
|
+
description { "access state" }
|
26
|
+
failure_message { "expected #{described_class} to access state #{field}" }
|
27
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `ApplicationOperation.state_reader`
|
4
|
+
#
|
5
|
+
# class SomeTask < ApplicationOperation
|
6
|
+
# state_reader :foo
|
7
|
+
# state_reader :bar
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# RSpec.describe SomeTask, type: :operation do
|
11
|
+
# subject { described_class.new(state) }
|
12
|
+
#
|
13
|
+
# let(:state) { Class.new(ApplicationState).new }
|
14
|
+
#
|
15
|
+
# it { is_expected.to read_state :foo }
|
16
|
+
# it { is_expected.to read_state :bar }
|
17
|
+
# end
|
18
|
+
|
19
|
+
RSpec::Matchers.define :read_state do |field|
|
20
|
+
match { |operation| expect(operation._state_readers).to include field }
|
21
|
+
description { "read state" }
|
22
|
+
failure_message { "expected #{described_class} to read state #{field}" }
|
23
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `ApplicationOperation.state_writer`
|
4
|
+
#
|
5
|
+
# class SomeTask < ApplicationOperation
|
6
|
+
# state_writer :foo
|
7
|
+
# state_writer :bar
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# RSpec.describe SomeTask, type: :operation do
|
11
|
+
# subject { described_class.new(state) }
|
12
|
+
#
|
13
|
+
# let(:state) { Class.new(ApplicationState).new }
|
14
|
+
#
|
15
|
+
# it { is_expected.to write_state :foo }
|
16
|
+
# it { is_expected.to write_state :bar }
|
17
|
+
# end
|
18
|
+
|
19
|
+
RSpec::Matchers.define :write_state do |field|
|
20
|
+
match { |operation| expect(operation._state_writers).to include field }
|
21
|
+
description { "write state" }
|
22
|
+
failure_message { "expected #{described_class} to write state #{field}" }
|
23
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
data/lib/flow/spec_helper.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "spicerack/spec_helper"
|
4
4
|
|
5
|
-
require_relative "
|
6
|
-
require_relative "
|
7
|
-
require_relative "
|
8
|
-
require_relative "
|
5
|
+
require_relative "rspec/custom_matchers"
|
6
|
+
require_relative "rspec/shared_contexts"
|
7
|
+
require_relative "rspec/shoulda_matcher_helper"
|
8
|
+
require_relative "rspec/configuration"
|
data/lib/flow/state/output.rb
CHANGED
data/lib/flow/state_base.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "state/errors/not_validated"
|
4
|
-
|
5
3
|
require_relative "state/status"
|
6
4
|
require_relative "state/output"
|
7
5
|
|
8
6
|
# A **State** is an aggregation of input and derived data.
|
9
7
|
module Flow
|
10
8
|
class StateBase < Spicerack::InputModel
|
11
|
-
include
|
12
|
-
include
|
9
|
+
include State::Status
|
10
|
+
include State::Output
|
13
11
|
end
|
14
12
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A **StateProxy** adapts a **State** to an **Operation** through an Operation's `state_*` accessors.
|
4
|
+
module Flow
|
5
|
+
class StateProxy
|
6
|
+
def initialize(state)
|
7
|
+
@state = state
|
8
|
+
end
|
9
|
+
|
10
|
+
# @deprecated
|
11
|
+
def method_missing(method_name, *arguments, &block)
|
12
|
+
return super unless state.respond_to?(method_name)
|
13
|
+
|
14
|
+
ActiveSupport::Deprecation.warn(
|
15
|
+
"Direct state access of `#{method_name}' on #{state.inspect} will be removed in a future version of flow. "\
|
16
|
+
"Use a state accessor instead - for more information see github/freshly/flow/deprecation_notice"
|
17
|
+
)
|
18
|
+
state.public_send(method_name, *arguments, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @deprecated
|
22
|
+
def respond_to_missing?(method_name, include_private = false)
|
23
|
+
state.respond_to?(method_name) || super
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :state
|
29
|
+
end
|
30
|
+
end
|
data/lib/flow/version.rb
CHANGED
@@ -3,15 +3,19 @@
|
|
3
3
|
require "rails_helper"
|
4
4
|
|
5
5
|
RSpec.describe ApplicationOperation, type: :operation do
|
6
|
-
subject
|
7
|
-
|
8
|
-
let(:state) { double }
|
6
|
+
subject { described_class }
|
9
7
|
|
10
8
|
it { is_expected.to inherit_from Flow::OperationBase }
|
11
9
|
|
12
10
|
describe "#execute" do
|
13
11
|
subject(:execute) { operation.execute }
|
14
|
-
|
15
|
-
|
12
|
+
|
13
|
+
context "when unsuccessful" do
|
14
|
+
pending "describe the effects of an unsuccessful `Operation#execute` (or delete) #{__FILE__}"
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when successful" do
|
18
|
+
pending "describe the effects of a successful `Operation#execute` (or delete) #{__FILE__}"
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
@@ -19,6 +19,10 @@ RSpec.describe <%= class_name %>, type: :operation do
|
|
19
19
|
|
20
20
|
it { is_expected.to inherit_from ApplicationOperation }
|
21
21
|
|
22
|
+
# it { is_expected.to access_state :foo }
|
23
|
+
# it { is_expected.to read_state :bar }
|
24
|
+
# it { is_expected.to write_state :baz }
|
25
|
+
|
22
26
|
describe "#execute" do
|
23
27
|
subject(:execute) { operation.execute }
|
24
28
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.10.
|
4
|
+
version: 0.10.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Garside
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2020-01-
|
15
|
+
date: 2020-01-08 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: activemodel
|
@@ -174,6 +174,20 @@ dependencies:
|
|
174
174
|
- - "~>"
|
175
175
|
- !ruby/object:Gem::Version
|
176
176
|
version: '1.3'
|
177
|
+
- !ruby/object:Gem::Dependency
|
178
|
+
name: bcrypt
|
179
|
+
requirement: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - "~>"
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: 3.1.13
|
184
|
+
type: :development
|
185
|
+
prerelease: false
|
186
|
+
version_requirements: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - "~>"
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: 3.1.13
|
177
191
|
- !ruby/object:Gem::Dependency
|
178
192
|
name: rspice
|
179
193
|
requirement: !ruby/object:Gem::Requirement
|
@@ -242,7 +256,6 @@ files:
|
|
242
256
|
- lib/flow/concerns/transaction_wrapper.rb
|
243
257
|
- lib/flow/flow/callbacks.rb
|
244
258
|
- lib/flow/flow/core.rb
|
245
|
-
- lib/flow/flow/errors/state_invalid.rb
|
246
259
|
- lib/flow/flow/flux.rb
|
247
260
|
- lib/flow/flow/operations.rb
|
248
261
|
- lib/flow/flow/status.rb
|
@@ -253,29 +266,30 @@ files:
|
|
253
266
|
- lib/flow/operation/callbacks.rb
|
254
267
|
- lib/flow/operation/core.rb
|
255
268
|
- lib/flow/operation/error_handler.rb
|
256
|
-
- lib/flow/operation/errors/already_executed.rb
|
257
|
-
- lib/flow/operation/errors/already_rewound.rb
|
258
269
|
- lib/flow/operation/execute.rb
|
259
270
|
- lib/flow/operation/failures.rb
|
260
271
|
- lib/flow/operation/status.rb
|
261
272
|
- lib/flow/operation/transactions.rb
|
262
273
|
- lib/flow/operation_base.rb
|
274
|
+
- lib/flow/rspec/configuration.rb
|
275
|
+
- lib/flow/rspec/custom_matchers.rb
|
276
|
+
- lib/flow/rspec/custom_matchers/access_state.rb
|
277
|
+
- lib/flow/rspec/custom_matchers/define_failure.rb
|
278
|
+
- lib/flow/rspec/custom_matchers/define_output.rb
|
279
|
+
- lib/flow/rspec/custom_matchers/handle_error.rb
|
280
|
+
- lib/flow/rspec/custom_matchers/have_on_state.rb
|
281
|
+
- lib/flow/rspec/custom_matchers/read_state.rb
|
282
|
+
- lib/flow/rspec/custom_matchers/use_operations.rb
|
283
|
+
- lib/flow/rspec/custom_matchers/wrap_in_transaction.rb
|
284
|
+
- lib/flow/rspec/custom_matchers/write_state.rb
|
285
|
+
- lib/flow/rspec/shared_contexts.rb
|
286
|
+
- lib/flow/rspec/shared_contexts/with_failing_operation.rb
|
287
|
+
- lib/flow/rspec/shoulda_matcher_helper.rb
|
263
288
|
- lib/flow/spec_helper.rb
|
264
|
-
- lib/flow/spec_helper/custom_matchers.rb
|
265
|
-
- lib/flow/spec_helper/custom_matchers/define_failure.rb
|
266
|
-
- lib/flow/spec_helper/custom_matchers/define_output.rb
|
267
|
-
- lib/flow/spec_helper/custom_matchers/handle_error.rb
|
268
|
-
- lib/flow/spec_helper/custom_matchers/have_on_state.rb
|
269
|
-
- lib/flow/spec_helper/custom_matchers/use_operations.rb
|
270
|
-
- lib/flow/spec_helper/custom_matchers/wrap_in_transaction.rb
|
271
|
-
- lib/flow/spec_helper/rspec_configuration.rb
|
272
|
-
- lib/flow/spec_helper/shared_contexts.rb
|
273
|
-
- lib/flow/spec_helper/shared_contexts/with_failing_operation.rb
|
274
|
-
- lib/flow/spec_helper/shoulda_matcher_helper.rb
|
275
|
-
- lib/flow/state/errors/not_validated.rb
|
276
289
|
- lib/flow/state/output.rb
|
277
290
|
- lib/flow/state/status.rb
|
278
291
|
- lib/flow/state_base.rb
|
292
|
+
- lib/flow/state_proxy.rb
|
279
293
|
- lib/flow/version.rb
|
280
294
|
- lib/generators/flow/USAGE
|
281
295
|
- lib/generators/flow/application_flow/USAGE
|
@@ -334,7 +348,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
334
348
|
- !ruby/object:Gem::Version
|
335
349
|
version: '0'
|
336
350
|
requirements: []
|
337
|
-
|
351
|
+
rubyforge_project:
|
352
|
+
rubygems_version: 2.7.6
|
338
353
|
signing_key:
|
339
354
|
specification_version: 4
|
340
355
|
summary: Write modular and reusable business logic that's understandable and maintainable.
|