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