flow 0.10.5 → 0.10.8

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