flow 0.9.3 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +177 -245
- data/lib/flow.rb +2 -0
- data/lib/flow/concerns/transaction_wrapper.rb +10 -17
- data/lib/flow/custom_matchers.rb +5 -3
- data/lib/flow/custom_matchers/define_failure.rb +21 -0
- data/lib/flow/custom_matchers/define_output.rb +34 -0
- data/lib/flow/custom_matchers/handle_error.rb +32 -0
- data/lib/flow/custom_matchers/have_on_state.rb +54 -0
- data/lib/flow/custom_matchers/use_operations.rb +13 -11
- data/lib/flow/custom_matchers/wrap_in_transaction.rb +60 -0
- data/lib/flow/flow/callbacks.rb +1 -1
- data/lib/flow/flow/core.rb +1 -0
- data/lib/flow/flow/flux.rb +0 -2
- data/lib/flow/flow/status.rb +0 -4
- data/lib/flow/flow/transactions.rb +2 -2
- data/lib/flow/flow/trigger.rb +1 -1
- data/lib/flow/flow_base.rb +13 -15
- data/lib/flow/operation/accessors.rb +66 -0
- data/lib/flow/operation/callbacks.rb +12 -10
- data/lib/flow/operation/core.rb +10 -8
- data/lib/flow/operation/error_handler.rb +18 -12
- data/lib/flow/operation/errors/already_executed.rb +5 -3
- data/lib/flow/operation/errors/already_rewound.rb +5 -3
- data/lib/flow/operation/execute.rb +28 -26
- data/lib/flow/operation/failures.rb +48 -42
- data/lib/flow/operation/status.rb +18 -21
- data/lib/flow/operation/transactions.rb +8 -6
- data/lib/flow/operation_base.rb +15 -13
- data/lib/flow/rspec_configuration.rb +5 -0
- data/lib/flow/spec_helper.rb +3 -0
- data/lib/flow/state/errors/not_validated.rb +9 -0
- data/lib/flow/state/output.rb +59 -0
- data/lib/flow/state/status.rb +22 -0
- data/lib/flow/state_base.rb +9 -16
- data/lib/flow/version.rb +1 -1
- data/lib/generators/flow/application_flow/templates/application_flow.rb +1 -1
- data/lib/generators/flow/application_operation/templates/application_operation.rb +1 -1
- data/lib/generators/flow/application_state/templates/application_state.rb +1 -1
- data/lib/generators/flow/operation/USAGE +1 -1
- data/lib/generators/flow/operation/templates/operation.rb.erb +0 -5
- data/lib/generators/flow/state/USAGE +1 -1
- data/lib/generators/flow/state/templates/state.rb.erb +2 -1
- data/lib/generators/rspec/application_flow/templates/application_flow_spec.rb +1 -1
- data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +1 -9
- data/lib/generators/rspec/application_state/templates/application_state_spec.rb +1 -1
- data/lib/generators/rspec/flow/templates/flow_spec.rb.erb +0 -8
- data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +5 -11
- data/lib/generators/rspec/state/templates/state_spec.rb.erb +8 -10
- metadata +39 -52
- data/lib/flow/custom_matchers/define_argument.rb +0 -19
- data/lib/flow/custom_matchers/define_attribute.rb +0 -19
- data/lib/flow/custom_matchers/define_option.rb +0 -26
- data/lib/flow/flow/ebb.rb +0 -28
- data/lib/flow/flow/revert.rb +0 -18
- data/lib/flow/operation/rewind.rb +0 -25
- data/lib/flow/state/arguments.rb +0 -30
- data/lib/flow/state/attributes.rb +0 -31
- data/lib/flow/state/callbacks.rb +0 -13
- data/lib/flow/state/core.rb +0 -14
- data/lib/flow/state/options.rb +0 -45
- data/lib/flow/state/string.rb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e881d33993ef70f023c82f8c595d77780487a67625c050849e1688c02dc4ccd
|
4
|
+
data.tar.gz: 7624fed517717e114ba494ce5ae93f252329ee704730a7579f02d4e20f7a53c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a07515e6214de6df0e568d18d3a04692d59d99aa50d8b3fb2ed5f89d394ed4ee0d78dcff05aabd86780df8484f240ca3cb2213bb37ad4e4bc5f8a17ec48454e
|
7
|
+
data.tar.gz: 2d09e0ca74029c270ae49a73287eef064669a5924cfec2fcd8d72ccf959edc6fa49f9625a4aad393213f24a39d498f990ddb3b451d6e08e84e67699c9c1afe9e
|
data/README.md
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
* [Operations](#operations)
|
14
14
|
* [States](#states)
|
15
15
|
* [Input](#input)
|
16
|
-
* [
|
16
|
+
* [Output](#output)
|
17
17
|
* [Derivative Data](#derivative-data)
|
18
18
|
* [State Concerns](#state-concerns)
|
19
19
|
* [Validations](#validations)
|
@@ -21,9 +21,6 @@
|
|
21
21
|
* [Exceptions](#exceptions)
|
22
22
|
* [Failures](#failures)
|
23
23
|
* [Callback Events](#callback-events)
|
24
|
-
* [Reverting a Flow](#reverting-a-flow)
|
25
|
-
* [Undoing Operations](#undoing-operations)
|
26
|
-
* [Manual Revert](#manual-revert)
|
27
24
|
* [Transactions](#transactions)
|
28
25
|
* [Around a Flow](#around-a-flow)
|
29
26
|
* [Around an Operation](#around-an-operation)
|
@@ -275,6 +272,23 @@ ExampleFlow.trigger(foo: :foo) # => ArgumentError (Missing argument: bar)
|
|
275
272
|
ExampleFlow.trigger(foo: :foo, bar: :bar) # => #<ExampleFlow:0x00007ff7b7d92ae0 ...>
|
276
273
|
```
|
277
274
|
|
275
|
+
By default, nil is a valid argument:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
ExampleFlow.trigger(foo: nil, bar: nil) # => #<ExampleFlow:0x10007ff7b7d92ae0 ...>
|
279
|
+
```
|
280
|
+
|
281
|
+
If you want to require a non-nil value for your argument, set the `allow_nil` option (`true` by default):
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
class ExampleState < ApplicationState
|
285
|
+
argument :foo
|
286
|
+
argument :bar, allow_nil: false
|
287
|
+
end
|
288
|
+
|
289
|
+
ExampleFlow.trigger(foo: nil, bar: nil) # => ArgumentError (Missing argument: bar)
|
290
|
+
```
|
291
|
+
|
278
292
|
**Options** describe input which may be provided to define or override the initial state.
|
279
293
|
|
280
294
|
Options can optionally define a default value.
|
@@ -303,28 +317,57 @@ state.favorite_color # => "1a1f1e"
|
|
303
317
|
state.favorite_foods # => ["avocado", "hummus" ,"nutritional_yeast"]
|
304
318
|
```
|
305
319
|
|
306
|
-
####
|
307
|
-
|
308
|
-
|
320
|
+
#### Output
|
321
|
+
|
322
|
+
Output data is created by Operations during runtime and CANNOT be validated or provided as part of the input. It can only be written once the state has been validated successfully, otherwise an error is raised.
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class ExampleState < ApplicationState
|
326
|
+
argument :name
|
327
|
+
|
328
|
+
validates :name, length: { minimum: 3 }
|
329
|
+
output :foo
|
330
|
+
end
|
331
|
+
|
332
|
+
state = ExampleState.new(name: "fe")
|
333
|
+
state.foo # => raises Flow::State::Errors::NotValidated
|
334
|
+
state.foo = :something # => raises Flow::State::Errors::NotValidated
|
335
|
+
|
336
|
+
state.valid? # => false
|
337
|
+
state.foo # => raises Flow::State::Errors::NotValidated
|
338
|
+
state.foo = :something # => raises Flow::State::Errors::NotValidated
|
309
339
|
|
310
|
-
|
340
|
+
state.name = "fefifofum"
|
341
|
+
state.valid? # => true
|
342
|
+
state.foo # => nil
|
343
|
+
state.foo = :something # => :something
|
344
|
+
state.outputs.foo # :something
|
345
|
+
```
|
346
|
+
|
347
|
+
Outputs can optionally define a default value.
|
348
|
+
|
349
|
+
If no default is specified, the value will be `nil`.
|
311
350
|
|
312
|
-
|
351
|
+
If the default value is static, it can be specified in the class definition.
|
352
|
+
|
353
|
+
If the default value is dynamic, you may provide a block to compute the default value.
|
354
|
+
|
355
|
+
⚠️ **Heads Up**: The default value blocks **DO NOT** provide access to the state or it's other variables!
|
313
356
|
|
314
357
|
```ruby
|
315
358
|
class ExampleState < ApplicationState
|
316
|
-
|
359
|
+
output :story, default: []
|
317
360
|
end
|
318
361
|
|
319
362
|
class AskAQuestion < ApplicationOperation
|
320
363
|
def behavior
|
321
|
-
state.
|
364
|
+
state.story << "Bah Bah, Black Sheep. Have you any wool?"
|
322
365
|
end
|
323
366
|
end
|
324
367
|
|
325
368
|
class GiveAnAnswer < ApplicationOperation
|
326
369
|
def behavior
|
327
|
-
state.
|
370
|
+
state.story << "Yes sir, yes sir! Three bags full!"
|
328
371
|
end
|
329
372
|
end
|
330
373
|
|
@@ -332,75 +375,64 @@ class ExampleFlow < ApplicationFlow
|
|
332
375
|
operations AskAQuestion, GiveAnAnswer
|
333
376
|
end
|
334
377
|
|
335
|
-
result = ExampleFlow.trigger
|
336
|
-
result.
|
337
|
-
# A conversation, for your consideration:
|
378
|
+
result = ExampleFlow.trigger
|
379
|
+
result.outputs.story.join("\n")
|
338
380
|
# Bah Bah, Black Sheep. Have you any wool?
|
339
381
|
# Yes sir, yes sir! Three bags full!
|
340
382
|
```
|
341
383
|
|
342
|
-
If you are
|
384
|
+
If you are creating something in your operation it's usually best practice to use [Transactions](#transactions) on either the Flow or Operation.
|
385
|
+
|
386
|
+
🙅 *Don't Make This Mistake*: Output is meant to capture data generated at runtime. Do not define data that COULD have been fetched in a standard state method into output:
|
343
387
|
|
344
388
|
```ruby
|
345
|
-
class
|
346
|
-
|
389
|
+
class BadState < ApplicationState
|
390
|
+
argument :foo
|
391
|
+
output :bar
|
347
392
|
end
|
348
393
|
|
349
|
-
class
|
394
|
+
class BadOperation < ApplicationOperation
|
350
395
|
def behavior
|
351
|
-
state.
|
352
|
-
end
|
353
|
-
|
354
|
-
def undo
|
355
|
-
state.the_foo.destroy!
|
356
|
-
state.the_foo = nil
|
396
|
+
state.foo = Bar.where(foo: foo)
|
357
397
|
end
|
358
398
|
end
|
359
399
|
```
|
360
400
|
|
361
|
-
|
401
|
+
Instead, always define data retrieval within the state and use output for created / generated data:
|
362
402
|
|
363
403
|
```ruby
|
364
|
-
class
|
365
|
-
|
366
|
-
|
404
|
+
class GoodState < ApplicationState
|
405
|
+
argument :foo
|
406
|
+
|
407
|
+
output :gaz
|
408
|
+
|
409
|
+
def bar
|
410
|
+
Bar.where(foo: foo)
|
411
|
+
end
|
412
|
+
memoize :bar
|
367
413
|
end
|
368
|
-
```
|
369
|
-
|
370
|
-
Under the hood `attribute` uses `attr_accessor` so you could override the default reader instead:
|
371
414
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
def the_foo
|
377
|
-
@the_foo ||= []
|
415
|
+
class GoodOperation < ApplicationOperation
|
416
|
+
def behavior
|
417
|
+
state.gaz = Gaz.create!(name: foo.name, count: bar.count)
|
378
418
|
end
|
379
419
|
end
|
380
420
|
```
|
381
421
|
|
382
|
-
|
422
|
+
🚨 *Don't Make This Mistake Either*: You may feel you want to "combine" some of you input data so it shows up in the output hash. **Well don't!** Output narrowly refers to a specific type of runtime created data. It's **not** some kind "result" hash. (Technically, it's not even a hash at all, it's a [Struct](https://ruby-doc.org/core-2.6.2/Struct.html)!)
|
383
423
|
|
384
|
-
|
424
|
+
If you want to define an explicit results payload, do so explicitly:
|
385
425
|
|
386
426
|
```ruby
|
387
|
-
class
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
427
|
+
class GoodState < ApplicationState
|
428
|
+
argument :foo
|
429
|
+
option :bar, default: :nar
|
430
|
+
output :gaz
|
431
|
+
|
432
|
+
def results
|
433
|
+
outputs.to_h.dup.merge(foo: foo, bar: bar)
|
434
|
+
end
|
393
435
|
end
|
394
|
-
|
395
|
-
accr_state = AccessorState.new
|
396
|
-
accr_state.foo = "some value!"
|
397
|
-
accr_state.to_s # => #<AccessorState >
|
398
|
-
accr_state.foo # => "some value!"
|
399
|
-
|
400
|
-
attr_state = AttributeState.new
|
401
|
-
attr_state.foo = "some value!"
|
402
|
-
attr_state.to_s # => #<AttributeState foo="some value!">
|
403
|
-
attr_state.foo # => "some value!"
|
404
436
|
```
|
405
437
|
|
406
438
|
#### Derivative Data
|
@@ -675,6 +707,21 @@ operation_failure11.problem # => :too_generous
|
|
675
707
|
operation_failure11.details.disappointment_level # => :wow_very_disappoint
|
676
708
|
```
|
677
709
|
|
710
|
+
You can also specify `if:` / `unless:` options to proactively trigger failures as guard clauses:
|
711
|
+
|
712
|
+
```ruby
|
713
|
+
class PassBottlesAround < ApplicationOperation
|
714
|
+
failure :too_dangerous, if: -> { state.bottle_of == "tequila" }
|
715
|
+
failure :not_dangerous_enough, unless: :drink_dangerous?
|
716
|
+
|
717
|
+
private
|
718
|
+
|
719
|
+
def drink_dangerous?
|
720
|
+
%[water juice soda].exclude? state.bottle_of
|
721
|
+
end
|
722
|
+
end
|
723
|
+
```
|
724
|
+
|
678
725
|
### Callback Events
|
679
726
|
|
680
727
|
Operations feature error events which are triggered when a problem occurs.
|
@@ -715,119 +762,6 @@ class OperationThree < ApplicationOperation
|
|
715
762
|
end
|
716
763
|
```
|
717
764
|
|
718
|
-
## Reverting a Flow
|
719
|
-
|
720
|
-
![Flow Revert](docs/images/revert.png)
|
721
|
-
|
722
|
-
When something goes wrong in Flow `#revert` is called.
|
723
|
-
|
724
|
-
This calls `#rewind` on Operations to `#undo` their behavior.
|
725
|
-
|
726
|
-
Reverting a Flow rewinds all its executed operations in reverse order (referred to as an **ebb**).
|
727
|
-
|
728
|
-
Reverting is automatic and happens by default. You cannot opt out of the revert process, but you can choose to not define any `#undo` methods on your Operations.
|
729
|
-
|
730
|
-
```ruby
|
731
|
-
class ExampleState < ApplicationState; end
|
732
|
-
|
733
|
-
class GenericOperation < ApplicationOperation
|
734
|
-
def behavior
|
735
|
-
puts "#{self.class.name}#behavior"
|
736
|
-
end
|
737
|
-
|
738
|
-
def undo
|
739
|
-
puts "#{self.class.name}#undo"
|
740
|
-
end
|
741
|
-
end
|
742
|
-
|
743
|
-
class ExampleFlow < ApplicationFlow
|
744
|
-
operations OperationOne, OperationTwo, OperationThree, OperationFour
|
745
|
-
end
|
746
|
-
|
747
|
-
class OperationOne < GenericOperation; end
|
748
|
-
class OperationTwo < GenericOperation; end
|
749
|
-
class OperationThree < GenericOperation
|
750
|
-
failure :bad_stuff
|
751
|
-
|
752
|
-
def behavior
|
753
|
-
super
|
754
|
-
bad_stuff_failure!
|
755
|
-
end
|
756
|
-
end
|
757
|
-
class OperationFour < GenericOperation; end
|
758
|
-
|
759
|
-
ExampleFlow.trigger
|
760
|
-
|
761
|
-
# Prints:
|
762
|
-
# OperationOne#behavior
|
763
|
-
# OperationTwo#behavior
|
764
|
-
# OperationThree#behavior
|
765
|
-
# OperationTwo#undo
|
766
|
-
# OperationOne#undo
|
767
|
-
```
|
768
|
-
|
769
|
-
⚠️ **Heads Up**: For the Operation that failed, `#undo` is **NOT** called. Only operations which execute successfully can be undone.
|
770
|
-
|
771
|
-
### Undoing Operations
|
772
|
-
|
773
|
-
```ruby
|
774
|
-
class ReserveQuantity < ApplicationOperation
|
775
|
-
delegate :product, :quantity, to: :state
|
776
|
-
delegate :available_inventory_count, to: :product
|
777
|
-
|
778
|
-
def behavior
|
779
|
-
product.update!(available_inventory_count: available_inventory_count - quantity)
|
780
|
-
end
|
781
|
-
|
782
|
-
def undo
|
783
|
-
product.update!(available_inventory_count: available_inventory_count + quantity)
|
784
|
-
end
|
785
|
-
end
|
786
|
-
```
|
787
|
-
|
788
|
-
💁 *Note*: If you omit the `#undo`, a revert will essentially pass over that Operation.
|
789
|
-
|
790
|
-
If your Operation should not be undone and you want it to halt reverting, call a defined failure in `#undo`.
|
791
|
-
|
792
|
-
```ruby
|
793
|
-
class ExampleOperation < ApplicationOperation
|
794
|
-
failure :irreversible_behavior
|
795
|
-
|
796
|
-
def behavior
|
797
|
-
PurchaseService.charge_customer(state.customer)
|
798
|
-
end
|
799
|
-
|
800
|
-
def undo
|
801
|
-
irreversible_behavior_failure!
|
802
|
-
end
|
803
|
-
end
|
804
|
-
```
|
805
|
-
|
806
|
-
### Manual Revert
|
807
|
-
|
808
|
-
Flows in which an error occur are reverted automatically.
|
809
|
-
|
810
|
-
You can also manually revert a completed flow, even if it was fully successful.
|
811
|
-
|
812
|
-
```ruby
|
813
|
-
class ExampleFlow < ApplicationFlow
|
814
|
-
operations OperationOne, OperationTwo, OperationThree, OperationFour
|
815
|
-
end
|
816
|
-
|
817
|
-
flow = SomeExampleFlow.trigger
|
818
|
-
# OperationOne#behavior
|
819
|
-
# OperationTwo#behavior
|
820
|
-
# OperationThree#behavior
|
821
|
-
# OperationFour#behavior
|
822
|
-
flow.success? # => true
|
823
|
-
flow.revert
|
824
|
-
# OperationFour#undo
|
825
|
-
# OperationThree#undo
|
826
|
-
# OperationTwo#undo
|
827
|
-
# OperationOne#undo
|
828
|
-
flow.reverted? # => true
|
829
|
-
```
|
830
|
-
|
831
765
|
## Transactions
|
832
766
|
|
833
767
|
![Flow Transactions](docs/images/transaction.png)
|
@@ -836,8 +770,6 @@ Flow features a callback driven approach to wrap business logic within database
|
|
836
770
|
|
837
771
|
Both **Flows** and **Operations** can be wrapped with a transaction.
|
838
772
|
|
839
|
-
🚨 *Be Aware*: Unless otherwise specified, transactions apply to **both** success **and** failure cases. You can pass `only:` or `except:` options to `wrap_in_transaction` to alter this behavior.
|
840
|
-
|
841
773
|
### Around a Flow
|
842
774
|
|
843
775
|
Flows where no operation should be persisted unless all are successful should use a transaction.
|
@@ -850,18 +782,6 @@ class ExampleFlow < ApplicationFlow
|
|
850
782
|
end
|
851
783
|
```
|
852
784
|
|
853
|
-
Flows can transaction wrap `:flux` (caused by `#trigger`) or `:ebb` (caused by `#revert`).
|
854
|
-
|
855
|
-
```ruby
|
856
|
-
class ExampleFlow < ApplicationFlow
|
857
|
-
wrap_in_transaction only: :flux
|
858
|
-
end
|
859
|
-
|
860
|
-
class ExampleFlow < ApplicationFlow
|
861
|
-
wrap_in_transaction except: :ebb
|
862
|
-
end
|
863
|
-
```
|
864
|
-
|
865
785
|
### Around an Operation
|
866
786
|
|
867
787
|
Operations which modify several persisted objects together should use a transaction.
|
@@ -876,32 +796,32 @@ class OperationTwo < ApplicationFlow
|
|
876
796
|
end
|
877
797
|
```
|
878
798
|
|
879
|
-
|
799
|
+
## Statuses
|
880
800
|
|
881
|
-
|
882
|
-
class ExampleOperation < ApplicationOperation
|
883
|
-
wrap_in_transaction only: :behavior
|
884
|
-
end
|
801
|
+
Flows, Operations, and States all have a set of predicate methods to describe their current status.
|
885
802
|
|
886
|
-
|
887
|
-
wrap_in_transaction except: :undo
|
888
|
-
end
|
889
|
-
```
|
803
|
+
### Flows
|
890
804
|
|
891
|
-
|
805
|
+
| Status | Description |
|
806
|
+
| ------------ | ------------------------- |
|
807
|
+
| `pending?` | `#trigger` not called. |
|
808
|
+
| `triggered?` | `#trigger` was called. |
|
809
|
+
| `failed?` | Some operation failed. |
|
810
|
+
| `success?` | All operations succeeded. |
|
892
811
|
|
893
|
-
|
812
|
+
### Operations
|
813
|
+
|
814
|
+
| Status | Description |
|
815
|
+
| ------------ | ------------------------- |
|
816
|
+
| `executed?` | `#execute` was called. |
|
817
|
+
| `failed?` | Execution failed. |
|
818
|
+
| `success?` | Execution succeeded. |
|
894
819
|
|
895
|
-
|
896
|
-
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
| Flow | `pending?` | `#trigger` not called. |
|
901
|
-
| Flow | `triggered?` | `#trigger` was called. |
|
902
|
-
| Flow | `failed?` | Some operation failed. |
|
903
|
-
| Flow | `success?` | All operations succeeded. |
|
904
|
-
| Flow | `reverted?` | `#revert` was called. |
|
820
|
+
### States
|
821
|
+
|
822
|
+
| Status | Description |
|
823
|
+
| ------------ | ------------------------- |
|
824
|
+
| `validated?` | `#vaild?` returned true. |
|
905
825
|
|
906
826
|
## Utilities
|
907
827
|
|
@@ -912,7 +832,7 @@ Flow offers a number of utilities which allow you to tap into and extend it's fu
|
|
912
832
|
Flows, Operations, and States all make use of [ActiveSupport::Callbacks](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html) to compose advanced functionality.
|
913
833
|
|
914
834
|
```ruby
|
915
|
-
class TakeBottlesDown <
|
835
|
+
class TakeBottlesDown < ApplicationOperation
|
916
836
|
set_callback(:execute, :before) { bottle_count_term }
|
917
837
|
set_callback(:execute, :after) { state.output.push("You take #{bottle_count_term} down.") }
|
918
838
|
|
@@ -934,13 +854,9 @@ The callbacks which are available on each class are:
|
|
934
854
|
| Flow | `:initialize` | When a new flow is being constructed. |
|
935
855
|
| Flow | `:trigger` | When `#trigger` is called on a flow. |
|
936
856
|
| Flow | `:flux` | When `#trigger` is called on a flow. |
|
937
|
-
| Flow | `:revert` | When `#revert` is called on a flow. |
|
938
|
-
| Flow | `:ebb` | When `#revert` is called on a flow. |
|
939
857
|
| State | `:initialize` | When a new state is being constructed. |
|
940
858
|
| Operation | `:execute` | When `#execute` is called. |
|
941
859
|
| Operation | `:behavior` | When `#execute` is called. |
|
942
|
-
| Operation | `:rewind` | When `#rewind` is called. |
|
943
|
-
| Operation | `:undo` | When `#rewind` is called. |
|
944
860
|
| Operation | `:failure` | When any type of error occurs. |
|
945
861
|
| Operation | `$problem` | When an error of type $problem occurs. |
|
946
862
|
|
@@ -951,7 +867,7 @@ Flow includes the very awesome [ShortCircuIt](https://github.com/Freshly/spicera
|
|
951
867
|
To leverage it, just add `memoize :method_name` to your Flows, Operations, or States.
|
952
868
|
|
953
869
|
```ruby
|
954
|
-
class TakeBottlesDown <
|
870
|
+
class TakeBottlesDown < ApplicationOperation
|
955
871
|
def bottle_count_term
|
956
872
|
return "it" if state.bottles.number_on_the_wall == 1
|
957
873
|
return "one" if state.taking_down_one?
|
@@ -979,7 +895,7 @@ The gems adds methods to Flows, Operations, and States which share names with lo
|
|
979
895
|
| `fatal` | Highly actionable and critical issues. |
|
980
896
|
|
981
897
|
```ruby
|
982
|
-
class ExampleOperation <
|
898
|
+
class ExampleOperation < ApplicationOperation
|
983
899
|
def behavior
|
984
900
|
warn(:nothing_to_do, { empty_object: obj }) and return if obj.empty?
|
985
901
|
|
@@ -1091,10 +1007,15 @@ end
|
|
1091
1007
|
```
|
1092
1008
|
|
1093
1009
|
This will allow you to use the following custom matchers:
|
1094
|
-
* [define_argument](lib/flow/custom_matchers/define_argument.rb)
|
1095
|
-
* [define_attribute](lib/flow/custom_matchers/define_attribute.rb)
|
1096
|
-
* [
|
1097
|
-
* [
|
1010
|
+
* [define_argument](lib/flow/custom_matchers/define_argument.rb) tests usage of `ApplicationState.argument`
|
1011
|
+
* [define_attribute](lib/flow/custom_matchers/define_attribute.rb) tests usage of `ApplicationState.attribute`
|
1012
|
+
* [define_failure](lib/flow/custom_matchers/define_failure.rb) tests usage of `ApplicationOperation.failure`
|
1013
|
+
* [define_option](lib/flow/custom_matchers/define_option.rb) tests usage of `ApplicationState.option`
|
1014
|
+
* [define_output](lib/flow/custom_matchers/define_output.rb) tests usage of `ApplicationState.output`
|
1015
|
+
* [handle_error](lib/flow/custom_matchers/handle_error.rb) tests usage of `ApplicationOperation.handle_error`
|
1016
|
+
* [use_operations](lib/flow/custom_matchers/use_operations.rb) tests usage of `ApplicationFlow.operations`
|
1017
|
+
* [wrap_in_transaction](lib/flow/custom_matchers/wrap_in_transaction.rb) tests usage of `.wrap_in_transaction` for `ApplicationFlow` or `ApplicationOperation`
|
1018
|
+
* [have_on_state](lib/flow/custom_matchers/have_on_state.rb) tests for data on State after a `ApplicationFlow` or `ApplicationOperation` has been run
|
1098
1019
|
|
1099
1020
|
### Testing Flows
|
1100
1021
|
|
@@ -1124,14 +1045,6 @@ RSpec.describe FooFlow, type: :flow do
|
|
1124
1045
|
|
1125
1046
|
pending "describe the effects of a successful `Flow#flux` (or delete) #{__FILE__}"
|
1126
1047
|
end
|
1127
|
-
|
1128
|
-
describe "#revert" do
|
1129
|
-
before { flow.trigger! }
|
1130
|
-
|
1131
|
-
subject(:revert) { flow.revert }
|
1132
|
-
|
1133
|
-
pending "describe the effects of a successful `Flow#ebb` (or delete) #{__FILE__}"
|
1134
|
-
end
|
1135
1048
|
end
|
1136
1049
|
```
|
1137
1050
|
|
@@ -1151,11 +1064,13 @@ require "rails_helper"
|
|
1151
1064
|
RSpec.describe MakeTheThingDoTheStuff, type: :operation do
|
1152
1065
|
subject(:operation) { described_class.new(state) }
|
1153
1066
|
|
1154
|
-
let(:state) { example_state_class.new(**state_input) }
|
1067
|
+
let(:state) { example_state_class.new(**state_input).tap(&:validate) }
|
1155
1068
|
let(:example_state_class) do
|
1156
1069
|
Class.new(ApplicationState) do
|
1157
1070
|
# argument :foo
|
1158
1071
|
# option :bar
|
1072
|
+
# attribute :baz
|
1073
|
+
# output :gaz
|
1159
1074
|
end
|
1160
1075
|
end
|
1161
1076
|
let(:state_input) do
|
@@ -1164,19 +1079,11 @@ RSpec.describe MakeTheThingDoTheStuff, type: :operation do
|
|
1164
1079
|
|
1165
1080
|
it { is_expected.to inherit_from ApplicationOperation }
|
1166
1081
|
|
1167
|
-
describe "#execute
|
1168
|
-
subject(:execute
|
1082
|
+
describe "#execute" do
|
1083
|
+
subject(:execute) { operation.execute }
|
1169
1084
|
|
1170
1085
|
pending "describe `Operation#behavior` (or delete) #{__FILE__}"
|
1171
1086
|
end
|
1172
|
-
|
1173
|
-
describe "#rewind" do
|
1174
|
-
before { operation.execute! }
|
1175
|
-
|
1176
|
-
subject(:execute!) { operation.rewind }
|
1177
|
-
|
1178
|
-
pending "describe `Operation#undo` (or delete) #{__FILE__}"
|
1179
|
-
end
|
1180
1087
|
end
|
1181
1088
|
```
|
1182
1089
|
|
@@ -1189,6 +1096,8 @@ let(:example_state_class) do
|
|
1189
1096
|
Class.new(ApplicationState) do
|
1190
1097
|
# argument :foo
|
1191
1098
|
# option :bar
|
1099
|
+
# attribute :baz
|
1100
|
+
# output :gaz
|
1192
1101
|
end
|
1193
1102
|
end
|
1194
1103
|
```
|
@@ -1211,6 +1120,7 @@ let(:example_state_class) do
|
|
1211
1120
|
argument :foo
|
1212
1121
|
option :bar
|
1213
1122
|
attribute :baz
|
1123
|
+
output :gaz
|
1214
1124
|
end
|
1215
1125
|
|
1216
1126
|
let(:state_input) do
|
@@ -1220,7 +1130,31 @@ let(:example_state_class) do
|
|
1220
1130
|
let(:foo) { ... }
|
1221
1131
|
let(:bar) { ... }
|
1222
1132
|
end
|
1223
|
-
```
|
1133
|
+
```
|
1134
|
+
|
1135
|
+
You are encouraged to use `execute`, rather than `execute!` in testing. You can trust that if an `operation_failure` is present, the operation _would_ have raised an `Operation::Failures::OperationFailure` if you used `execute!`.
|
1136
|
+
|
1137
|
+
Doing so will allow you to make assertions on the failure without having to expect errors:
|
1138
|
+
```ruby
|
1139
|
+
class SomeOperation < ApplicationOperation
|
1140
|
+
failure :somethings_invalid
|
1141
|
+
|
1142
|
+
def behavior
|
1143
|
+
somethings_invalid_failure! baz: "relevant data" if state.foo == "something invalid"
|
1144
|
+
end
|
1145
|
+
end
|
1146
|
+
```
|
1147
|
+
|
1148
|
+
```ruby
|
1149
|
+
let(:state_input) { foo: "something invalid" }
|
1150
|
+
|
1151
|
+
before { operation.execute }
|
1152
|
+
|
1153
|
+
it "fails with the expected data" do
|
1154
|
+
expect(operation.operation_failure.problem).to eq :somethings_invalid
|
1155
|
+
expect(operation.operation_failure.details.baz).to eq "relevant data"
|
1156
|
+
end
|
1157
|
+
```
|
1224
1158
|
|
1225
1159
|
### Testing States
|
1226
1160
|
|
@@ -1234,19 +1168,17 @@ States are generated with the following RSPec template:
|
|
1234
1168
|
require "rails_helper"
|
1235
1169
|
|
1236
1170
|
RSpec.describe FooState, type: :state do
|
1237
|
-
subject(:state) { described_class
|
1238
|
-
|
1239
|
-
let(:input) do
|
1240
|
-
{}
|
1241
|
-
end
|
1171
|
+
subject(:state) { described_class }
|
1242
1172
|
|
1243
1173
|
it { is_expected.to inherit_from ApplicationState }
|
1244
|
-
# it { is_expected.to define_argument :
|
1245
|
-
# it { is_expected.to
|
1246
|
-
# it { is_expected.to define_option(:
|
1247
|
-
# it { is_expected.to define_option(:
|
1174
|
+
# it { is_expected.to define_argument :required_input }
|
1175
|
+
# it { is_expected.to define_argument :necessary_input, allow_nil: false }
|
1176
|
+
# it { is_expected.to define_option(:optional_input) }
|
1177
|
+
# it { is_expected.to define_option(:option_with_default, default: :default_static_value) }
|
1178
|
+
# it { is_expected.to define_option(:option_with_default_from_block, default: default_block_value) }
|
1248
1179
|
# it { is_expected.to validate_presence_of ... }
|
1249
|
-
# it { is_expected.to
|
1180
|
+
# it { is_expected.to define_output :foo }
|
1181
|
+
# it { is_expected.to define_output :foo, default: :bar }
|
1250
1182
|
end
|
1251
1183
|
```
|
1252
1184
|
|