flow 0.10.2 → 0.10.7.1.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +107 -1116
  3. data/lib/flow.rb +17 -2
  4. data/lib/flow/flow/callbacks.rb +7 -5
  5. data/lib/flow/flow/core.rb +16 -14
  6. data/lib/flow/flow/flux.rb +36 -32
  7. data/lib/flow/flow/operations.rb +16 -14
  8. data/lib/flow/flow/status.rb +15 -13
  9. data/lib/flow/flow/transactions.rb +7 -5
  10. data/lib/flow/flow/trigger.rb +26 -24
  11. data/lib/flow/flow_base.rb +5 -6
  12. data/lib/flow/malfunction/base.rb +10 -0
  13. data/lib/flow/operation/core.rb +12 -1
  14. data/lib/flow/operation/error_handler.rb +1 -1
  15. data/lib/flow/operation/execute.rb +1 -1
  16. data/lib/flow/operation_base.rb +10 -15
  17. data/lib/flow/{spec_helper/rspec_configuration.rb → rspec/configuration.rb} +0 -0
  18. data/lib/flow/{spec_helper → rspec}/custom_matchers.rb +3 -0
  19. data/lib/flow/rspec/custom_matchers/access_state.rb +27 -0
  20. data/lib/flow/{spec_helper → rspec}/custom_matchers/define_failure.rb +0 -0
  21. data/lib/flow/{spec_helper → rspec}/custom_matchers/define_output.rb +0 -0
  22. data/lib/flow/{spec_helper → rspec}/custom_matchers/handle_error.rb +0 -0
  23. data/lib/flow/{spec_helper → rspec}/custom_matchers/have_on_state.rb +6 -1
  24. data/lib/flow/rspec/custom_matchers/read_state.rb +23 -0
  25. data/lib/flow/{spec_helper → rspec}/custom_matchers/use_operations.rb +0 -0
  26. data/lib/flow/{spec_helper → rspec}/custom_matchers/wrap_in_transaction.rb +0 -0
  27. data/lib/flow/rspec/custom_matchers/write_state.rb +23 -0
  28. data/lib/flow/{spec_helper → rspec}/shared_contexts.rb +0 -0
  29. data/lib/flow/{spec_helper → rspec}/shared_contexts/with_failing_operation.rb +0 -0
  30. data/lib/flow/{spec_helper → rspec}/shoulda_matcher_helper.rb +0 -0
  31. data/lib/flow/spec_helper.rb +4 -4
  32. data/lib/flow/state/output.rb +1 -1
  33. data/lib/flow/state_base.rb +5 -4
  34. data/lib/flow/state_proxy.rb +30 -0
  35. data/lib/flow/version.rb +1 -1
  36. data/lib/generators/flow/operation/templates/operation.rb.erb +4 -0
  37. data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +9 -5
  38. data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +4 -0
  39. metadata +73 -66
  40. data/lib/flow/flow/errors/state_invalid.rb +0 -7
  41. data/lib/flow/operation/errors/already_executed.rb +0 -9
  42. data/lib/flow/operation/errors/already_rewound.rb +0 -9
  43. 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: fb00b50b5c0c19093beaf572f6e050e578b58fd83d5641f1b1dac553cb5096c5
4
- data.tar.gz: 17ceb71b234ee7aebf897ff08ed11c2db68a1c63a9338322a9619f896c53e705
3
+ metadata.gz: 29339c930e7018a613c2474b03c59f3c3e03f059141c5c1100045bdfad35aacb
4
+ data.tar.gz: b5912c43449ea1a71f72fa002bb038488679914685a5c8245760a59291503c70
5
5
  SHA512:
6
- metadata.gz: 57537f1e3d84c95cc6018dafa17db38c71bc2b0ef606f25e67c9e875a9ca6196f604240e693cf808a227862266d6c5fc3b19c0edcfe8b29c3ae793ddbd8b37a0
7
- data.tar.gz: 93386f65a3b4fc066c2d9d82b68be1f103290aaa63a7c0b356f0d8d9f5a6290aa80aaa8706da42450c3aff1f9904d56c280b11e0d00dc6b7f1c73a18ab2519dd
6
+ metadata.gz: 6cd852054927adc10ff9b7bd4983b65f7f70c4532e08a1ed4a936ed52967b6778043a023f6442afcfca4437e0567e678bc2f403f538b73db4c5c3f789a5a8644
7
+ data.tar.gz: e7dd9ac6284e3f4e7dbbe6882cfd1e74932707d661eabfbb05034b240f302198a632e3e9ad921dd12309130f1b8d9eea591af772ef7f1ba49437f0ef3990afa4
data/README.md CHANGED
@@ -1,45 +1,10 @@
1
1
  # Flow
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/flow.svg)](https://badge.fury.io/rb/flow)
4
- [![Build Status](https://semaphoreci.com/api/v1/freshly/flow/branches/master/badge.svg)](https://semaphoreci.com/freshly/flow)
4
+ [![Build Status](https://semaphoreci.com/api/v1/freshly/flow/branches/main/badge.svg)](https://semaphoreci.com/freshly/flow)
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/02131658005b10c289e0/maintainability)](https://codeclimate.com/github/Freshly/flow/maintainability)
6
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/02131658005b10c289e0/test_coverage)](https://codeclimate.com/github/Freshly/flow/test_coverage)
7
7
 
8
- * [Installation](#installation)
9
- * [Getting Started](#getting-started)
10
- * [What is Flow?](#what-is-flow)
11
- * [How it Works](#how-it-works)
12
- * [Flows](#flows)
13
- * [Operations](#operations)
14
- * [States](#states)
15
- * [Input](#input)
16
- * [Output](#output)
17
- * [Derivative Data](#derivative-data)
18
- * [State Concerns](#state-concerns)
19
- * [Validations](#validations)
20
- * [Errors](#errors)
21
- * [Exceptions](#exceptions)
22
- * [Failures](#failures)
23
- * [Callback Events](#callback-events)
24
- * [Transactions](#transactions)
25
- * [Around a Flow](#around-a-flow)
26
- * [Around an Operation](#around-an-operation)
27
- * [Statuses](#statuses)
28
- * [Utilities](#utilities)
29
- * [Callbacks](#callbacks)
30
- * [Memoization](#memoization)
31
- * [Logging](#logging)
32
- * [Inheritance](#inheritance)
33
- * [Testing](#testing)
34
- * [Testing Setup](#testing-setup)
35
- * [Testing Flows](#testing-flows)
36
- * [Testing Operations](#testing-operations)
37
- * [Testing States](#testing-states)
38
- * [Integration Testing](#integration-testing)
39
- * [Contributing](#contributing)
40
- * [Development](#development)
41
- * [License](#license)
42
-
43
8
  ## Installation
44
9
 
45
10
  Add this line to your application's Gemfile:
@@ -55,1183 +20,209 @@ $ bundle install
55
20
  $ rails generate flow:install
56
21
  ```
57
22
 
58
- ## Getting Started
59
-
60
- Flow comes with some nice rails generators. You are encouraged to use them!
61
-
62
- ```bash
63
- $ rails generate flow Foo
64
- invoke state
65
- invoke rspec
66
- create spec/states/foo_state_spec.rb
67
- create app/states/foo_state.rb
68
- invoke rspec
69
- create spec/flows/foo_flow_spec.rb
70
- create app/flows/foo_flow.rb
71
- $ rails generate flow:state Bar
72
- invoke rspec
73
- create spec/states/bar_state_spec.rb
74
- create app/states/bar_state.rb
75
- $ rails generate flow:operation MakeTheThingDoTheStuff
76
- invoke rspec
77
- create spec/operations/make_the_thing_do_the_stuff_spec.rb
78
- create app/operations/make_the_thing_do_the_stuff.rb
79
- ```
80
-
81
23
  ## What is Flow?
82
24
 
83
25
  Flow is a [SOLID](https://en.wikipedia.org/wiki/SOLID) implementation of the [Command Pattern](https://en.wikipedia.org/wiki/Command_pattern) for Ruby on Rails.
84
26
 
85
27
  Flows allow you to encapsulate your application's [business logic](http://en.wikipedia.org/wiki/Business_logic) into a set of extensible and reusable objects.
86
28
 
87
- ## How it Works
88
-
89
- ![Flow Basics](docs/images/flow.png)
90
-
91
- There are three important concepts to distinguish here: [Flows](#Flows), [Operations](#Operations), and [States](#States).
92
-
93
- ### Flows
94
-
95
- A **Flow** is a collection of procedurally executed **Operations** sharing a common **State**.
96
-
97
- All Flows should be named with the `Flow` suffix (ex: `FooFlow`).
98
-
99
- ```ruby
100
- class CalculateTimetablesFlow < ApplicationFlow
101
- operations ClearExistingTimetables,
102
- CalculateTimetables,
103
- SummarizeTimetables,
104
- DestroyEmptyTimetableCells
105
- end
106
- ```
107
-
108
- The `operations` are an ordered list of the behaviors which are executed with (and possibly change) the Flow's state.
109
-
110
- Flows accept input representing the arguments and options which define the initial state.
111
-
112
- ```ruby
113
- CalculateTimetablesFlow.trigger(timeframe: Day.today)
114
- ```
115
-
116
- Triggering a Flow executes all its operations in sequential order if **and only if** it has a valid state.
117
-
118
- When `#trigger` is called on a Flow, `#execute` is called on Operations sequentially in their given order (referred to as a **flux**).
119
-
120
- Unless otherwise specified a **Flow** assumes its state class shares a common name.
121
-
122
- Ex: `FooBarBazFlow` assumes there is a defined `FooBarBazState`.
123
-
124
- If you want to customize this behavior, define the state class explicitly:
125
-
126
- ```ruby
127
- class ExampleFlow < ApplicationFlow
128
- def self.state_class
129
- MyCoolState
130
- end
131
- end
132
- ```
133
-
134
- If you _already have_ an instance of a state class that you want to execute a Flow on, you can simply pass it directly to trigger:
135
-
136
- ```ruby
137
- state_instance = ExampleState.new(...)
138
-
139
- CalculateTimetablesFlow.trigger(state_instance)
140
- ```
141
-
142
- ### Operations
29
+ ## Quickstart Example
143
30
 
144
- An **Operation** is a service object which is executed with a **State**.
145
-
146
- Operations should **not** be named with the `Operation` suffix; name them what they do!
147
-
148
- ```ruby
149
- class ClearExistingTimetables < ApplicationOperation
150
- def behavior
151
- state.existing_timetable_cells.update_all(total_minutes: 0)
152
- end
153
- end
154
- ```
155
-
156
- ```ruby
157
- class CalculateTimetables < ApplicationOperation
158
- def behavior
159
- state.minutes_by_project_employee.each do |project_employee, total_minutes|
160
- project_id, employee_id = project_employee
161
- timetable = state.timeframe.timetables.find_or_create_by!(project_id: project_id)
162
- cell = timetable.cells.find_or_create_by!(employee_id: employee_id)
163
-
164
- cell.update!(total_minutes: total_minutes)
165
- end
166
- end
167
- end
168
- ```
169
-
170
- ```ruby
171
- class SummarizeTimetables < ApplicationOperation
172
- def behavior
173
- state.timetables.each do |timetable|
174
- timetable.update!(total_minutes: timetable.cells.sum(:total_minutes))
175
- end
176
- end
177
- end
178
- ```
179
-
180
- ```ruby
181
- class DestroyEmptyTimetableCells < ApplicationOperation
182
- def behavior
183
- state.empty_cells.destroy_all
184
- end
185
- end
186
- ```
187
-
188
- Operations take a state as input and define a `#behavior` that occurs when `#execute` is called.
189
-
190
- 💁‍ *Pro Tip*: Operations are just objects! They can be used outside of Flows. Just give them a State (or a State-like object) and you can use them in isolation!
191
-
192
- ```ruby
193
- class ExampleOperation < ApplicationOperation
194
- def behavior
195
- puts "Hello, #{state.first_name}"
196
- end
197
- end
198
-
199
- operation = ExampleOperation.new(OpenStruct.new(first_name: "Eric"))
200
- operation.execute
201
- # Hello, Eric
202
- operation.executed? # => true
203
- ```
204
-
205
- ### States
206
-
207
- A **State** is an aggregation of input and derived data.
208
-
209
- All States should be named with the `State` suffix (ex: `FooState`).
210
-
211
- ```ruby
212
- class CalculateTimetablesState < ApplicationState
213
- argument :timeframe
214
-
215
- def existing_timetable_cells
216
- @existing_timetable_cells ||= TimetableCell.where(timetable: existing_timetables)
217
- end
218
-
219
- def minutes_by_project_employee
220
- @minutes_by_project_employee ||= data_by_employee_project.transform_values do |values|
221
- values.sum(&:total_minutes)
222
- end
223
- end
224
-
225
- def timetables
226
- @timetables ||= Timetable.where(project_id: project_ids)
227
- end
228
-
229
- def empty_cells
230
- @empty_cells ||= TimetableCell.
231
- joins(:timetable).
232
- where(total_minutes: 0, timetables: { project_id: project_ids })
233
- end
234
-
235
- private
236
-
237
- delegate :timesheets, to: :timeframe
238
-
239
- def existing_timetables
240
- @existing_timetables ||= timeframe.timetables.where(project_id: project_ids)
241
- end
242
-
243
- def project_ids
244
- @project_ids ||= timesheet_data.map(&:project_id).uniq
245
- end
246
-
247
- def data_by_employee_project
248
- @data_by_employee_project ||= timesheet_data.group_by do |data|
249
- [ data.project_id, data.employee_id ]
250
- end
251
- end
252
-
253
- def timesheet_data
254
- @timesheet_data ||= timesheets.
255
- reportable.
256
- summarizable.
257
- joins(:timeclock).
258
- select("timeclocks.project_id, timeclocks.employee_id, timesheets.total_minutes")
259
- end
260
- end
261
- ```
262
-
263
- #### Input
264
-
265
- A state accepts input represented by **arguments** and **options** which initialize it.
266
-
267
- **Arguments** describe input required to define the initial state.
268
-
269
- If any arguments are missing, an `ArgumentError` is raised.
270
-
271
- ```ruby
272
- class ExampleFlow < ApplicationFlow; end
273
- class ExampleState < ApplicationState
274
- argument :foo
275
- argument :bar
276
- end
277
-
278
- ExampleFlow.trigger # => ArgumentError (Missing arguments: foo, bar)
279
- ExampleFlow.trigger(foo: :foo) # => ArgumentError (Missing argument: bar)
280
- ExampleFlow.trigger(foo: :foo, bar: :bar) # => #<ExampleFlow:0x00007ff7b7d92ae0 ...>
281
- ```
282
-
283
- By default, nil is a valid argument:
284
-
285
- ```ruby
286
- ExampleFlow.trigger(foo: nil, bar: nil) # => #<ExampleFlow:0x10007ff7b7d92ae0 ...>
287
- ```
288
-
289
- If you want to require a non-nil value for your argument, set the `allow_nil` option (`true` by default):
290
-
291
- ```ruby
292
- class ExampleState < ApplicationState
293
- argument :foo
294
- argument :bar, allow_nil: false
295
- end
296
-
297
- ExampleFlow.trigger(foo: nil, bar: nil) # => ArgumentError (Missing argument: bar)
298
- ```
299
-
300
- **Options** describe input which may be provided to define or override the initial state.
301
-
302
- Options can optionally define a default value.
303
-
304
- If no default is specified, the value will be `nil`.
305
-
306
- If the default value is static, it can be specified in the class definition.
307
-
308
- If the default value is dynamic, you may provide a block to compute the default value.
309
-
310
- ⚠️‍ **Heads Up**: The default value blocks **DO NOT** provide access to the state or its other variables!
311
-
312
- ```ruby
313
- class ExampleFlow < ApplicationFlow; end
314
- class ExampleState < ApplicationState
315
- option :attribution_source
316
- option :favorite_foods, default: %w[pizza ice_cream gluten]
317
- option(:favorite_color) { SecureRandom.hex(3) }
318
- end
319
-
320
- result = ExampleFlow.trigger(favorite_foods: %w[avocado hummus nutritional_yeast])
321
- state = result.state
322
-
323
- state.attribution_source # => nil
324
- state.favorite_color # => "1a1f1e"
325
- state.favorite_foods # => ["avocado", "hummus" ,"nutritional_yeast"]
326
- ```
327
-
328
- #### Output
329
-
330
- 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.
331
-
332
- ```ruby
333
- class ExampleState < ApplicationState
334
- argument :name
335
-
336
- validates :name, length: { minimum: 3 }
337
- output :foo
338
- end
339
-
340
- state = ExampleState.new(name: "fe")
341
- state.foo # => raises Flow::State::Errors::NotValidated
342
- state.foo = :something # => raises Flow::State::Errors::NotValidated
343
-
344
- state.valid? # => false
345
- state.foo # => raises Flow::State::Errors::NotValidated
346
- state.foo = :something # => raises Flow::State::Errors::NotValidated
347
-
348
- state.name = "fefifofum"
349
- state.valid? # => true
350
- state.foo # => nil
351
- state.foo = :something # => :something
352
- state.outputs.foo # :something
353
- ```
354
-
355
- Outputs can optionally define a default value.
356
-
357
- If no default is specified, the value will be `nil`.
358
-
359
- If the default value is static, it can be specified in the class definition.
360
-
361
- If the default value is dynamic, you may provide a block to compute the default value.
362
-
363
- ⚠️‍ **Heads Up**: The default value blocks **DO NOT** provide access to the state or it's other variables!
364
-
365
- ```ruby
366
- class ExampleState < ApplicationState
367
- output :story, default: []
368
- end
369
-
370
- class AskAQuestion < ApplicationOperation
371
- def behavior
372
- state.story << "Bah Bah, Black Sheep. Have you any wool?"
373
- end
374
- end
375
-
376
- class GiveAnAnswer < ApplicationOperation
377
- def behavior
378
- state.story << "Yes sir, yes sir! Three bags full!"
379
- end
380
- end
381
-
382
- class ExampleFlow < ApplicationFlow
383
- operations AskAQuestion, GiveAnAnswer
384
- end
385
-
386
- result = ExampleFlow.trigger
387
- result.outputs.story.join("\n")
388
- # Bah Bah, Black Sheep. Have you any wool?
389
- # Yes sir, yes sir! Three bags full!
390
- ```
391
-
392
- If you are creating something in your operation it's usually best practice to use [Transactions](#transactions) on either the Flow or Operation.
393
-
394
- 🙅‍ *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:
395
-
396
- ```ruby
397
- class BadState < ApplicationState
398
- argument :foo
399
- output :bar
400
- end
401
-
402
- class BadOperation < ApplicationOperation
403
- def behavior
404
- state.foo = Bar.where(foo: foo)
405
- end
406
- end
407
- ```
408
-
409
- Instead, always define data retrieval within the state and use output for created / generated data:
410
-
411
- ```ruby
412
- class GoodState < ApplicationState
413
- argument :foo
414
-
415
- output :gaz
416
-
417
- def bar
418
- Bar.where(foo: foo)
419
- end
420
- memoize :bar
421
- end
422
-
423
- class GoodOperation < ApplicationOperation
424
- def behavior
425
- state.gaz = Gaz.create!(name: foo.name, count: bar.count)
426
- end
427
- end
428
- ```
429
-
430
- 🚨 *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)!)
431
-
432
- If you want to define an explicit results payload, do so explicitly:
433
-
434
- ```ruby
435
- class GoodState < ApplicationState
436
- argument :foo
437
- option :bar, default: :nar
438
- output :gaz
439
-
440
- def results
441
- outputs.to_h.dup.merge(foo: foo, bar: bar)
442
- end
443
- end
31
+ Install Flow to your Rails project:
32
+ ```bash
33
+ $ rails generate flow:install
444
34
  ```
445
35
 
446
- #### Derivative Data
36
+ Then define `State`, `Operation`(s), and `Flow` objects.
447
37
 
448
- States provide you with a clear place to put any logic related to pre-processing of data.
38
+ ### State
449
39
 
450
- They also can help give developers a clear picture of how your data fits together:
451
-
452
- ```ruby
453
- class ExampleState < ApplicationState
454
- argument :user
40
+ A `State` object defines data that is to be read or written in `Operation` objects throughout the `Flow`. There are several types of data that can be defined, such as `argument`, `option`, and `output`.
455
41
 
456
- def most_actionable_order
457
- editable_orders.order(ship_date: :desc).first
458
- end
459
-
460
- private
461
-
462
- def editable_orders
463
- user.orders.unshipped.paid
464
- end
465
- end
42
+ ```bash
43
+ $ rails generate flow:state Charge
466
44
  ```
467
45
 
468
- #### State Concerns
469
-
470
- The architecture of each Flow having it's own state introduces a code reuse constraint.
471
-
472
- Consider the following example:
473
-
474
46
  ```ruby
475
- class MyExampleState < ApplicationState
476
- argument :user
477
-
478
- def most_actionable_order
479
- editable_orders.order(ship_date: :desc).first
480
- end
481
-
482
- private
483
-
484
- def editable_orders
485
- user.orders.unshipped.paid
486
- end
487
- end
47
+ # app/states/charge_state.rb
488
48
 
489
- class MyOtherExampleState < ApplicationState
49
+ class ChargeState < ApplicationState
50
+ # @!attribute [r]
51
+ # Order hash, readonly, required
52
+ argument :order
53
+ # @!attribute [r]
54
+ # User model instance readonly, required
490
55
  argument :user
491
-
492
- def least_actionable_order
493
- editable_orders.order(ship_date: :desc).last
494
- end
495
-
496
- private
497
-
498
- def editable_orders
499
- user.orders.unshipped.paid
500
- end
501
- end
502
- ```
503
-
504
- The recommended way to share common code between your states is by using concerns.
505
-
506
- For example, we could create `app/states/concerns/actionable_user_orders.rb`:
507
-
508
- ```ruby
509
- module ActionableUserOrders
510
- extend ActiveSupport::Concern
511
-
512
- included do
513
- argument :user
514
- end
515
-
516
- private
517
-
518
- def orders_by_ship_data
519
- editable_orders.order(ship_date: :desc)
520
- end
521
-
522
- def editable_orders
523
- user.orders.unshipped.paid
524
- end
525
- end
526
- ```
527
56
 
528
- Then your states become nice and clean:
57
+ # @!attribute [r]
58
+ # PaymentMethod model instance readonly, optional
59
+ option :payment_method
529
60
 
530
- ```ruby
531
- class MyExampleState < ApplicationState
532
- include ActionableUserOrders
533
-
534
- def most_actionable_order
535
- orders_by_ship_data.first
536
- end
537
- end
538
-
539
- class MyOtherExampleState < ApplicationState
540
- include ActionableUserOrders
541
-
542
- def least_actionable_order
543
- orders_by_ship_data.last
544
- end
61
+ # @!attribute [rw]
62
+ # Charge model instance readwrite, required
63
+ output :charge
545
64
  end
546
65
  ```
547
66
 
548
- #### Validations
549
-
550
- States are ActiveModels which means they have access to [ActiveModel::Validations](https://api.rubyonrails.org/classes/ActiveModel/Validations.html).
551
-
552
- It is considered a best practice to write validations in your states.
553
-
554
- Flows which have an invalid state will NOT execute any Operations, so it is inherently the safest and clearest way to proactively communicate about missed expectations.
555
-
556
- 💁‍ **Pro Tip**: There is a `trigger!` method on Flows that will raise certain errors that are normally silenced. Invalid states are one such example!
557
-
558
- ```ruby
559
- class ExampleFlow < ApplicationFlow; end
560
- class ExampleState < ApplicationState
561
- argument :first_name
562
-
563
- validates :first_name, length: { minimum: 2 }
564
- end
565
-
566
- ExampleFlow.trigger!(first_name: "a") # => raises Flow::Errors::StateInvalid
567
-
568
- result = ExampleFlow.trigger(first_name: "a")
569
- result.success? # => false
570
- result.failed? # => false
571
- result.triggered? # => false
572
- result.state.errors.messages # => {:first_name=>["is too short (minimum is 2 characters)"]}
573
- ```
574
-
575
- ## Errors
576
-
577
- ![Flow Errors](docs/images/error.png)
578
-
579
- When `#execute` is unsuccessful, expected problems are **failures** and unexpected problems are **Exceptions**.
580
-
581
- Errors handling can be either either **proactive** or **reactive**; ideally all errors that can be are *proactive*.
582
-
583
- **Proactive** error handling is a form of defensive programming. Instead of letting an error occur, you fail with a very clear signal as to why. Explicit failures are more desirable than than letting unexpected behavior dictate the program flow.
584
-
585
- **Reactive** error handling should be used to handle areas of the code where you do not control the underlying behaviors, such as integrations with third party gems. When you know something you can't prevent could happen, you can define a reactive error handler to cleanly translate an *exception* into a *failure*.
586
-
587
- ### Exceptions
588
-
589
- When an exception is raised during during execution, but a handler can rescue, it causes a failure instead.
590
-
591
- Otherwise, an unhandled exception will raise through both the Operation and Flow.
592
- `
593
- ```ruby
594
- class ExampleState < ApplicationState
595
- argument :number
596
- end
597
-
598
- class ExampleOperation < ApplicationOperation
599
- handle_error RuntimeError
600
-
601
- def behavior
602
- raise (state.number % 2 == 0 ? StandardError : RuntimeError)
603
- end
604
- end
605
-
606
- class ExampleFlow < ApplicationFlow
607
- operations ExampleOperation
608
- end
609
-
610
- ExampleFlow.trigger(number: 0) # => raises StandardError
611
- result = ExampleFlow.trigger(number: 1)
612
- result.failed? # => true
613
-
614
- operation_failure = result.failed_operation.operation_failure
615
- operation_failure.problem # => :runtime_error
616
- operation_failure.details.exception # => #<RuntimeError: RuntimeError>
617
- ```
618
-
619
- Handlers are inherited. They are searched from right to left, from bottom to top, and up the hierarchy. The handler of the first class for which exception.is_a?(klass) holds true is the one invoked, if any.
620
-
621
- If no problem is specified explicitly, a demodulized underscored version of the error is used.
622
-
623
- ```ruby
624
- class ExampleOperation < ApplicationOperation
625
- handle_error RuntimeError, problem: :something_bad_happened
626
- handle_error ActiveRecord::RecordInvalid
627
-
628
- def behavior
629
- raise (state.number % 2 == 0 ? ActiveRecord::RecordInvalid : RuntimeError)
630
- end
631
- end
632
-
633
- result0 = ExampleFlow.trigger(number: 0)
634
- operation_failure = result0.failed_operation.operation_failure
635
- operation_failure.problem # => :record_invalid
636
- operation_failure.details.exception # => #<ActiveRecord::RecordInvalid: Record invalid>
637
-
638
- result1 = ExampleFlow.trigger(number: 1)
639
- result1.failed_operation.operation_failure.problem # => :something_bad_happened
640
- ```
67
+ ### Operations
641
68
 
642
- You can also provide handlers in the form of either a block or a method name:
69
+ `Operation` objects execute some procedure defined in a `#behavior` method and can read and write to `State` data via defined accessor methods.
643
70
 
644
- ```ruby
645
- class ExampleOperation < ApplicationOperation
646
- handle_error RuntimeError, with: :handle_some_error
647
- handle_error ActiveRecord::RecordInvalid do
648
- # Do something here
649
- end
650
-
651
- def behavior
652
- raise (state.number % 2 == 0 ? ActiveRecord::RecordInvalid : RuntimeError)
653
- end
654
-
655
- private
656
-
657
- def handle_some_error
658
- # Do something different here
659
- end
660
- end
71
+ ```bash
72
+ $ rails generate flow:operation CreateCharge
661
73
  ```
662
74
 
663
- ### Failures
664
-
665
- In theory, failures should *never* occur in your Flows. Any guard clause you can put inside of an Operation to proactively fail you should be able to put inside of the state as a validation.
666
-
667
- In practice, failures will *always* occur in your Flows. Any sufficiently large organization will receive contributions from developers of all skill and business-specific knowledge levels. The suggested use of one State class per Flow means that if every state is responsible for proactive validation, you will eventually have a misstep and forget to include it.
668
-
669
- Having your Operation proactively fail is an example of [contract programming](https://en.wikipedia.org/wiki/Design_by_contract) and provides developers with a clear and non-brittle expectation of how it should be used.
670
-
671
- From a conceptual standpoint, you should consider your Operations as the most atomic expression of your business logic. Flows, and (by extension) the States that support them, are most effective when built up around a well defined set of Operations.
672
-
673
- When your system has multiple consistent ways to defend against corrupt data or prevent executions that generate exceptions, it's robust not redundant.
674
-
675
- `</rant>`
676
-
677
- Failures are part of the class definition of your Operation.
678
-
679
75
  ```ruby
680
- class PassBottlesAround < ApplicationOperation
681
- failure :too_generous
682
-
683
- def behavior
684
- too_generous_failure! if state.number_to_take_down >= 4
685
- end
686
- end
687
- ```
688
-
689
- When you define a failure a `#{failure_name}_failure!` method is defined for you.
690
-
691
- Calling this `_failure!` method will raise an exception which Flow handles by default, meaning it will not be raised as an exception from the Flow.
76
+ # app/operations/create_charge.rb
692
77
 
693
- An unstructured hash of data can be provided to the `_failure!` method and will be available in the `operation_failure` object:
78
+ class CreateCharge < ApplicationOperation
79
+ # @!attribute [r]
80
+ # Order hash, readonly
81
+ state_reader :order
82
+ # @!attribute [r]
83
+ # User model instance readonly
84
+ state_reader :user
85
+ # @!attribute [r]
86
+ # PaymentMethod model instance readonly
87
+ state_reader :payment_method
694
88
 
695
- ```ruby
696
- class PassBottlesAround < ApplicationOperation
697
- failure :too_generous
89
+ # @!attribute [rw]
90
+ # Charge model instance readwrite
91
+ state_writer :charge
698
92
 
699
93
  def behavior
700
- if state.number_to_take_down >= 4
701
- disappointment_level = state.number_to_take_down >= 10 ? :wow_very_disappoint : :am_disappoint
702
- too_generous_failure!(disappointment_level: disappointment_level)
703
- end
94
+ state.charge = Charge.create(payment_method: payment_method, order: order, user: user)
704
95
  end
705
- end
706
-
707
- result5 = ExampleFlow.trigger(number_to_take_down: 5)
708
- operation_failure5 = result5.failed_operation.operation_failure
709
- operation_failure5.problem # => :too_generous
710
- operation_failure5.details.disappointment_level # => :am_disappoint
711
-
712
- result11 = ExampleFlow.trigger(number_to_take_down: 11)
713
- operation_failure11 = result11.failed_operation.operation_failure
714
- operation_failure11.problem # => :too_generous
715
- operation_failure11.details.disappointment_level # => :wow_very_disappoint
716
- ```
717
-
718
- You can also specify `if:` / `unless:` options to proactively trigger failures as guard clauses:
719
96
 
720
- ```ruby
721
- class PassBottlesAround < ApplicationOperation
722
- failure :too_dangerous, if: -> { state.bottle_of == "tequila" }
723
- failure :not_dangerous_enough, unless: :drink_dangerous?
724
-
725
97
  private
726
-
727
- def drink_dangerous?
728
- %[water juice soda].exclude? state.bottle_of
729
- end
730
- end
731
- ```
732
98
 
733
- ### Callback Events
734
-
735
- Operations feature error events which are triggered when a problem occurs.
736
-
737
- This works for explictly defined failures:
738
-
739
- ```ruby
740
- class OperationOne < ApplicationOperation
741
- failure :too_greedy
742
-
743
- on_too_greedy_failure do
744
- SlackClient.send_message(:engineering, "Someones trying to give away too much stuff!")
99
+ def payment_method
100
+ payment_method.present? payment_method : user.default_payment_method
745
101
  end
746
102
  end
747
103
  ```
748
104
 
749
- As well as manually handled errors (using the demodulized underscored name of the error):
105
+ Use failure methods when an `Operation` and `Flow` should fail and no longer run:
750
106
 
751
- ```ruby
752
- class OperationTwo < ApplicationOperation
753
- handle_error ActiveRecord::RecordInvalid
754
-
755
- on_record_invalid_failure do
756
- Redis.incr("operation_two:invalid_records")
757
- end
758
- end
107
+ ```bash
108
+ $ rails generate flow:operation SubmitCharge
759
109
  ```
760
110
 
761
- You can also listen for any problems using the generic failure event:
762
-
763
111
  ```ruby
764
- class OperationThree < ApplicationOperation
765
- handle_error RuntimeError
766
-
767
- on_failure do
768
- EngineeringMailer.on_runtime_error(self.class.name)
769
- end
770
- end
771
- ```
772
-
773
- ## Transactions
774
-
775
- ![Flow Transactions](docs/images/transaction.png)
112
+ # app/operations/submit_charge.rb
776
113
 
777
- Flow features a callback driven approach to wrap business logic within database transaction.
778
-
779
- Both **Flows** and **Operations** can be wrapped with a transaction.
780
-
781
- ### Around a Flow
782
-
783
- Flows where no operation should be persisted unless all are successful should use a transaction.
784
-
785
- ```ruby
786
- class ExampleFlow < ApplicationFlow
787
- wrap_in_transaction
788
-
789
- operations OperationOne, OperationTwo, OperationThree
790
- end
791
- ```
114
+ class SubmitCharge < ApplicationOperation
115
+ # @!method charge_unsuccessful_failure!(data = {})
116
+ # Raises, stops the Operation and Flow, takes unstructured data hash
117
+ failure :charge_unsuccessful
792
118
 
793
- ### Around an Operation
119
+ # @!attribute [rw]
120
+ # Charge model instance read only
121
+ state_reader :charge
794
122
 
795
- Operations which modify several persisted objects together should use a transaction.
796
-
797
- ```ruby
798
- class OperationTwo < ApplicationFlow
799
- wrap_in_transaction
800
-
801
123
  def behavior
802
- # do a thing
803
- end
804
- end
805
- ```
806
-
807
- ## Statuses
808
-
809
- Flows, Operations, and States all have a set of predicate methods to describe their current status.
810
-
811
- ### Flows
812
-
813
- | Status | Description |
814
- | ------------ | ------------------------- |
815
- | `pending?` | `#trigger` not called. |
816
- | `triggered?` | `#trigger` was called. |
817
- | `failed?` | Some operation failed. |
818
- | `success?` | All operations succeeded. |
819
-
820
- ### Operations
821
-
822
- | Status | Description |
823
- | ------------ | ------------------------- |
824
- | `executed?` | `#execute` was called. |
825
- | `failed?` | Execution failed. |
826
- | `success?` | Execution succeeded. |
827
-
828
- ### States
829
-
830
- | Status | Description |
831
- | ------------ | ------------------------- |
832
- | `validated?` | `#vaild?` returned true. |
833
-
834
- ## Utilities
835
-
836
- Flow offers a number of utilities which allow you to tap into and extend it's functionality.
837
-
838
- ### Callbacks
839
-
840
- Flows, Operations, and States all make use of [ActiveSupport::Callbacks](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html) to compose advanced functionality.
841
-
842
- ```ruby
843
- class TakeBottlesDown < ApplicationOperation
844
- set_callback(:execute, :before) { bottle_count_term }
845
- set_callback(:execute, :after) { state.output.push("You take #{bottle_count_term} down.") }
846
-
847
- def bottle_count_term
848
- return "it" if state.bottles.number_on_the_wall == 1
849
- return "one" if state.taking_down_one?
850
-
851
- state.number_to_take_down
852
- end
853
- end
854
- ```
855
-
856
- Please consult the `ActiveSupport::Callbacks` documentation for guidance on how to use them.
857
-
858
- The callbacks which are available on each class are:
859
-
860
- | Class Name | Callbacks | Fired When... |
861
- | ------------- | ------------- | -------------------------------------- |
862
- | Flow | `:initialize` | When a new flow is being constructed. |
863
- | Flow | `:trigger` | When `#trigger` is called on a flow. |
864
- | Flow | `:flux` | When `#trigger` is called on a flow. |
865
- | State | `:initialize` | When a new state is being constructed. |
866
- | Operation | `:execute` | When `#execute` is called. |
867
- | Operation | `:behavior` | When `#execute` is called. |
868
- | Operation | `:failure` | When any type of error occurs. |
869
- | Operation | `$problem` | When an error of type $problem occurs. |
870
-
871
- ### Memoization
872
-
873
- Flow includes the very awesome [ShortCircuIt](https://github.com/Freshly/spicerack/tree/develop/short_circu_it) gem.
124
+ charge_unsuccessful_failure!(response_body: response.body) unless success?
874
125
 
875
- To leverage it, just add `memoize :method_name` to your Flows, Operations, or States.
876
-
877
- ```ruby
878
- class TakeBottlesDown < ApplicationOperation
879
- def bottle_count_term
880
- return "it" if state.bottles.number_on_the_wall == 1
881
- return "one" if state.taking_down_one?
882
-
883
- state.number_to_take_down
126
+ charge.update(success: true)
884
127
  end
885
- memoize :bottle_count_term
886
- end
887
- ```
888
-
889
- Consult the documentation for `ShorCircuIt` for more info on how to use it.
890
-
891
- ### Logging
892
-
893
- Flow includes the [Technologic](https://github.com/Freshly/spicerack/tree/develop/technologic) gem.
894
128
 
895
- The gems adds methods to Flows, Operations, and States which share names with log levels.
896
-
897
- | Level | Used For |
898
- | ------- | -------------------------------------- |
899
- | `debug` | Extra data; usually off in production. |
900
- | `info` | Standard data you always want to have. |
901
- | `warn` | Unexpected (but not exceptional) data. |
902
- | `error` | Exceptional cases representing issues. |
903
- | `fatal` | Highly actionable and critical issues. |
904
-
905
- ```ruby
906
- class ExampleOperation < ApplicationOperation
907
- def behavior
908
- warn(:nothing_to_do, { empty_object: obj }) and return if obj.empty?
909
-
910
- debug(:doing_a_thing)
911
-
912
- results = do_thing
913
-
914
- log(:did_a_thing, results: results)
915
- end
916
- end
917
- ```
918
-
919
- Flows and States come with automated out-of-the-box logging.
920
-
921
- The following is an example of what is logged without any extra log lines:
922
-
923
- ```text
924
- I, [2019-03-06T12:31:06.008329 #25951] INFO -- : {:event=>"trigger_started.CalculateWorksheetsFlow"}
925
- I, [2019-03-06T12:31:06.008551 #25951] INFO -- : {:event=>"execute_started.AssignCommitsToWorksheet"}
926
- I, [2019-03-06T12:31:07.402005 #25951] INFO -- : {:event=>"execute_finished.AssignCommitsToWorksheet", :duration=>1.393346}
927
- I, [2019-03-06T12:31:07.402217 #25951] INFO -- : {:event=>"execute_started.AssignCommentsToWorksheet"}
928
- I, [2019-03-06T12:31:07.438144 #25951] INFO -- : {:event=>"execute_finished.AssignCommentsToWorksheet"}
929
- I, [2019-03-06T12:31:07.438235 #25951] INFO -- : {:event=>"trigger_finished.CalculateWorksheetsFlow", :duration=>1.429788}
930
- ```
931
-
932
- Consult the documentation for `Technologic` for more info on how to use it.
933
-
934
- ## Inheritance
935
-
936
- Flows, Operations, and States all support inheritance of class definitions.
937
-
938
- ```ruby
939
- class ParentState < ApplicationState
940
- argument :foo
941
- end
942
-
943
- class ChildState < ParentState
944
- argument :bar
945
- end
946
-
947
- class ChildFlow < ApplicationFlow; end
948
- ChildFlow.trigger(bar: :bar) # => ArgumentError (Missing argument: foo)
949
- ```
950
-
951
- A common pattern in Flow is to use inheritance to DRY and conceptually related flows.
952
-
953
- Take for example the case of `Calculation` and `Recalculation`
954
-
955
- ```ruby
956
- class CalculateFooFlow < ApplicationFlow
957
- operations ClearOldFooCalculations, CaclulcateFoo, EmailFooReport
958
- end
129
+ private
959
130
 
960
- class CalculateFooState < ApplicationState
961
- def foos
962
- Foo.where(calculated_at: nil)
131
+ def success?
132
+ response.body.success == "true"
963
133
  end
964
- end
965
134
 
966
- # =================
967
-
968
- class RecalculateFooFlow < CalculateFooFlow; end
969
- class RecalculateFooState < ApplicationState
970
- def foos
971
- Foo.all
135
+ def response
136
+ PaymentProcessorClient.submit_charge(charge)
972
137
  end
138
+ memoize :response
973
139
  end
974
140
  ```
975
141
 
976
- There communicates that there is no difference between the Flows other than their States!
977
-
978
- ## Testing
979
-
980
- If you plan on writing `RSpec` tests `Flow` comes packaged with some custom matchers.
981
-
982
- ### Testing Setup
142
+ ### Flow
983
143
 
984
- Add the following to your `spec/rails_helper.rb` file:
144
+ A `Flow` object is composed of one or more ordered `Operation`s. Changes to the state will persist from one `Operation` to the next:
985
145
 
986
- ```ruby
987
- require "flow/spec_helper"
146
+ ```bash
147
+ $ rails generate flow Charge
988
148
  ```
989
149
 
990
- Flow works best with [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) and [rspice](https://github.com/Freshly/spicerack/tree/develop/rspice).
991
-
992
- Add those to the `development` and `test` group of your Gemfile:
993
-
994
150
  ```ruby
995
- group :development, :test do
996
- gem "shoulda-matchers", git: "https://github.com/thoughtbot/shoulda-matchers.git", branch: "rails-5"
997
- gem "rspice"
998
- end
999
- ```
1000
-
1001
- Then run `bundle install` and add the following into `spec/rails_helper.rb`:
151
+ # app/flow/charge_flow.rb
1002
152
 
1003
- ```ruby
1004
- require "rspec/rails"
1005
- require "rspice"
1006
- require "flow/spec_helper"
1007
-
1008
- # Configuration for the shoulda-matchers gem
1009
- Shoulda::Matchers.configure do |config|
1010
- config.integrate do |with|
1011
- with.test_framework :rspec
1012
- with.library :rails
1013
- end
153
+ class ChargeFlow < ApplicationFlow
154
+ operations CreateCharge,
155
+ SubmitCharge
1014
156
  end
1015
157
  ```
1016
158
 
1017
- This will allow you to use the following custom matchers:
1018
- * [define_argument](lib/flow/custom_matchers/define_argument.rb) tests usage of `ApplicationState.argument`
1019
- * [define_attribute](lib/flow/custom_matchers/define_attribute.rb) tests usage of `ApplicationState.attribute`
1020
- * [define_failure](lib/flow/custom_matchers/define_failure.rb) tests usage of `ApplicationOperation.failure`
1021
- * [define_option](lib/flow/custom_matchers/define_option.rb) tests usage of `ApplicationState.option`
1022
- * [define_output](lib/flow/custom_matchers/define_output.rb) tests usage of `ApplicationState.output`
1023
- * [handle_error](lib/flow/custom_matchers/handle_error.rb) tests usage of `ApplicationOperation.handle_error`
1024
- * [use_operations](lib/flow/custom_matchers/use_operations.rb) tests usage of `ApplicationFlow.operations`
1025
- * [wrap_in_transaction](lib/flow/custom_matchers/wrap_in_transaction.rb) tests usage of `.wrap_in_transaction` for `ApplicationFlow` or `ApplicationOperation`
1026
- * [have_on_state](lib/flow/custom_matchers/have_on_state.rb) tests for data on State after a `ApplicationFlow` or `ApplicationOperation` has been run
1027
-
1028
- ### Testing Flows
159
+ ### Usage
1029
160
 
1030
- The best way to test a Flow is with an integration test.
1031
-
1032
- The easiest way to test a Flow is with a unit test.
1033
-
1034
- Flow are generated with the following RSPec template:
161
+ Trigger the `Flow` in your code with `State` inputs:
1035
162
 
1036
163
  ```ruby
1037
- # frozen_string_literal: true
1038
-
1039
- require "rails_helper"
1040
-
1041
- RSpec.describe FooFlow, type: :flow do
1042
- subject(:flow) { described_class.new(**input) }
164
+ flow_input = {
165
+ order: order,
166
+ user: current_user,
167
+ payment_method: visa_credit_card,
168
+ }
1043
169
 
1044
- let(:input) do
1045
- {}
1046
- end
1047
-
1048
- it { is_expected.to inherit_from ApplicationFlow }
1049
- # it { is_expected.to use_operations ExampleOperation }
1050
-
1051
- describe "#trigger" do
1052
- subject(:trigger) { flow.trigger! }
1053
-
1054
- pending "describe the effects of a successful `Flow#flux` (or delete) #{__FILE__}"
1055
- end
1056
- end
170
+ flow = ChargeFlow.trigger(flow_input)
1057
171
  ```
1058
172
 
1059
- ### Testing Operations
1060
-
1061
- The easiest and best way to test an Operation is with a unit test.
1062
-
1063
- Operation unit tests work best when you treat them like integration tests! (Read: **No Mocking!**)
1064
-
1065
- Operations are generated with the following RSpec template:
1066
-
1067
- ```ruby
1068
- # frozen_string_literal: true
1069
-
1070
- require "rails_helper"
1071
-
1072
- RSpec.describe MakeTheThingDoTheStuff, type: :operation do
1073
- subject(:operation) { described_class.new(state) }
173
+ Arguments defined on `State` are required when triggering a `Flow`, options are optional:
1074
174
 
1075
- let(:state) { example_state_class.new(**state_input).tap(&:validate) }
1076
- let(:example_state_class) do
1077
- Class.new(ApplicationState) do
1078
- # argument :foo
1079
-
1080
- # output :gaz
1081
- end
1082
- end
1083
- let(:state_input) do
1084
- {}
1085
- end
1086
-
1087
- it { is_expected.to inherit_from ApplicationOperation }
1088
-
1089
- describe "#execute" do
1090
- subject(:execute) { operation.execute }
1091
-
1092
- pending "describe `Operation#behavior` (or delete) #{__FILE__}"
1093
- end
1094
- end
1095
175
  ```
1096
-
1097
- ⚠️ *Warning*: You have to do a little work to write a good test state!
1098
-
1099
- In the boilerplate from the generator, there is the following snippet:
1100
-
1101
- ```ruby
1102
- let(:example_state_class) do
1103
- Class.new(ApplicationState) do
1104
- # argument :foo
1105
-
1106
- # output :gaz
1107
- end
1108
- end
176
+ > ChargeFlow.trigger({})
177
+ ArgumentError: Missing arguments: order, user
1109
178
  ```
1110
179
 
1111
- By default, your operation specs are broken! The reason for this is to encourage resilient test writing.
180
+ State output can be accessed from the flow instance:
1112
181
 
1113
- Let's say that you have an Operation in your system called `CreateFoo` which is part of the `CreateFooFlow` and therefore is only ever called with a `CreateFooState`. You may be tempted to write something like:
1114
-
1115
- ```ruby
1116
- let(:example_state_class) { CreateFooState }
1117
182
  ```
183
+ > flow = ChargeFlow.trigger(flow_input)
1118
184
 
1119
- You are heavily encouraged *not* to do that. If your Operation is used by several different Flows, you don't want to have your test arbitrarily using some state for the test.
1120
-
1121
- Instead, use the spec as a way to communicate the contract of the Operation with the next developer. By boiling out a very clean example state that only includes what is necessary for the operation, you provide clear guidance on what the Operation's minimum requirements for a state are in a very transparent way.
1122
-
1123
- However, the emphasis is on _minimum_ requirements. `Option`s and `attribute`s are therefore discouraged in example states for operation specs, as well are contexts for when optional input is not provided. An operation designed for use with a State using optional inputs will _always_ require those methods to be available on its state class, so example states should always use arguments to define input.
1124
-
1125
- ```ruby
1126
- let(:example_state_class) do
1127
- Class.new(ApplicationState) do
1128
- argument :foo
1129
- argument :bar
1130
-
1131
- output :gaz
1132
- end
1133
-
1134
- let(:state_input) do
1135
- { foo: foo }
1136
- end
1137
-
1138
- let(:foo) { ... }
1139
- let(:bar) { nil }
1140
- end
185
+ > flow.state.charge
186
+ => #<Charge:0x00007fd5c5cda080 ... >
1141
187
  ```
1142
188
 
1143
- 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!`.
189
+ Success of the triggered `Flow` can be determined with these methods:
1144
190
 
1145
- Doing so will allow you to make assertions on the failure without having to expect errors:
1146
- ```ruby
1147
- class SomeOperation < ApplicationOperation
1148
- failure :somethings_invalid
1149
-
1150
- def behavior
1151
- somethings_invalid_failure! baz: "relevant data" if state.foo == "something invalid"
1152
- end
1153
- end
1154
191
  ```
192
+ > flow.success?
193
+ => true
1155
194
 
1156
- ```ruby
1157
- let(:state_input) { foo: "something invalid" }
1158
-
1159
- before { operation.execute }
1160
-
1161
- it "fails with the expected data" do
1162
- expect(operation.operation_failure.problem).to eq :somethings_invalid
1163
- expect(operation.operation_failure.details.baz).to eq "relevant data"
1164
- end
195
+ > flow.failed?
196
+ => false
1165
197
  ```
1166
198
 
1167
- ### Testing States
1168
-
1169
- The easiest and best way to test a State is with a unit test.
1170
-
1171
- States are generated with the following RSPec template:
199
+ If the `Flow` fails you can see the failures on the instance:
1172
200
 
1173
- ```ruby
1174
- # frozen_string_literal: true
1175
-
1176
- require "rails_helper"
1177
-
1178
- RSpec.describe FooState, type: :state do
1179
- subject(:state) { described_class }
1180
-
1181
- it { is_expected.to inherit_from ApplicationState }
1182
- # it { is_expected.to define_argument :required_input }
1183
- # it { is_expected.to define_argument :necessary_input, allow_nil: false }
1184
- # it { is_expected.to define_option(:optional_input) }
1185
- # it { is_expected.to define_option(:option_with_default, default: :default_static_value) }
1186
-
1187
- # let(:default_block_value) { SecureRandom.uuid }
1188
- # before { allow(SecureRandom).to receive(:uuid).and_return(default_block_value) }
1189
- # it { is_expected.to define_option(:option_with_default_from_block, default: default_block_value) }
1190
-
1191
- # it { is_expected.to validate_presence_of ... }
1192
- # it { is_expected.to define_output :foo }
1193
- # it { is_expected.to define_output :foo, default: :bar }
1194
- end
1195
201
  ```
202
+ # some flow that results in a failure...
203
+ > flow = ChargeFlow.trigger(flow_input)
1196
204
 
1197
- 💡 **Reminder**: You need to install `shoulda-matchers` to use things like `.to validate_presence_of ...`, `rspice` for `.to inherit_from ...`, and the `flow/spec_helper.rb` for `define_argument` and the like.
205
+ > flow.success?
206
+ => false
1198
207
 
1199
- ### Integration Testing
208
+ # get the failure
209
+ > flow.operation_failure.problem
210
+ => :charge_unsuccessful
1200
211
 
1201
- The best integration tests are the simplest!
1202
-
1203
- Create a state of the universe and confirm your flow changes it.
1204
-
1205
- ```ruby
1206
- describe "integration test" do
1207
- subject(:flow) { ChangePreferencesFlow.trigger(user: user, favorite_food: new_favorite_food) }
1208
-
1209
- let(:user) { create :user, favorite_food: original_favorite_food }
1210
- let(:original_favorite_food) { Faker::Lorem.unique.word }
1211
- let(:new_favorite_food) { Faker::Lorem.unique.word }
1212
-
1213
- it "changes User#favorite_food" do
1214
- expect { flow }.
1215
- to change { user.favorite_food }.
1216
- from(original_favorite_food).
1217
- to(new_favorite_food)
1218
- end
1219
- end
212
+ # access unstructured hash passed into failure method
213
+ > flow.operation_failure.details.response_body
214
+ => { some_response_body_here: ... }
1220
215
  ```
1221
216
 
1222
- 💡 *Note*: It's considered a best practice to put your integration test in the same file as the unit test.
1223
-
1224
- And always remember: **Good integration tests don't use mocking**!
1225
-
1226
- ## Contributing
217
+ ## Wiki
1227
218
 
1228
- Bug reports and pull requests are welcome on GitHub at https://github.com/freshly/flow.
219
+ Learn more with our wiki [Getting Started](https://github.com/Freshly/flow/wiki/Getting-Started#installation) page.
1229
220
 
1230
- ### Development
221
+ You also can download wiki to have offline access.
222
+ Just simply do:
1231
223
 
1232
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
224
+ `git clone git@github.com:Freshly/flow.wiki.git`
1233
225
 
1234
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1235
226
 
1236
227
  ## License
1237
228