activeinteractor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/README.md +661 -0
- data/lib/active_interactor.rb +63 -0
- data/lib/active_interactor/base.rb +18 -0
- data/lib/active_interactor/configuration.rb +34 -0
- data/lib/active_interactor/context.rb +300 -0
- data/lib/active_interactor/interactor.rb +97 -0
- data/lib/active_interactor/interactor/callbacks.rb +273 -0
- data/lib/active_interactor/interactor/context.rb +59 -0
- data/lib/active_interactor/interactor/execution.rb +25 -0
- data/lib/active_interactor/interactor/worker.rb +89 -0
- data/lib/active_interactor/organizer.rb +47 -0
- data/lib/active_interactor/version.rb +7 -0
- data/lib/rails/generators/active_interactor.rb +35 -0
- data/lib/rails/generators/active_interactor/install_generator.rb +31 -0
- data/lib/rails/generators/interactor/interactor_generator.rb +13 -0
- data/lib/rails/generators/interactor/organizer_generator.rb +18 -0
- data/lib/rails/generators/interactor/rspec_generator.rb +15 -0
- data/lib/rails/generators/interactor/test_unit_generator.rb +15 -0
- data/lib/rails/generators/templates/application_interactor.erb +4 -0
- data/lib/rails/generators/templates/initializer.rb +8 -0
- data/lib/rails/generators/templates/interactor.erb +12 -0
- data/lib/rails/generators/templates/organizer.erb +10 -0
- data/lib/rails/generators/templates/rspec.erb +7 -0
- data/lib/rails/generators/templates/test_unit.erb +9 -0
- metadata +269 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc5562711e71b13317b0a916339266074063ef6858399c436aa99287557e94ca
|
4
|
+
data.tar.gz: babc70027c7ca3fd1996368ac5ea24abdad6ef148f2151df47f9af810d5c06cd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: def28dc17ba2159abceb6775f874f00117ca0437d6871d74f8bc518e2fe712df8895cc9d46ca6fd5c18b464fde46a7572bec385e995c58e04af5498f09a18d33
|
7
|
+
data.tar.gz: 4785f15e5c9c5f43b81466a96aa14886e20712ed7a889530d4fd32456f4207c479304de6d3df6c1b33e3992873a2f974776e7f650fc032f71a82606304155ebf
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog],
|
6
|
+
and this project adheres to [Semantic Versioning].
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## v0.1.0 - 2019-03-30
|
11
|
+
|
12
|
+
- Initial gem release
|
13
|
+
|
14
|
+
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
15
|
+
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
16
|
+
|
17
|
+
[Unreleased]: https://github.com/aaronmallen/activeinteractor/compare/v0.1.0...HEAD
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Aaron Allen
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,661 @@
|
|
1
|
+
# ActiveInteractor
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/activeinteractor)
|
4
|
+
[](https://github.com/aaronmallen/activeinteractor/blob/master/LICENSE)
|
5
|
+
[](https://depfu.com/github/aaronmallen/activeinteractor)
|
6
|
+
|
7
|
+
[](https://www.travis-ci.com/aaronmallen/activeinteractor)
|
8
|
+
[](https://codeclimate.com/github/aaronmallen/activeinteractor/maintainability)
|
9
|
+
[](https://codeclimate.com/github/aaronmallen/activeinteractor/test_coverage)
|
10
|
+
|
11
|
+
Ruby interactors with [ActiveModel::Validations] based on the [interactors][collective_idea_interactors] gem.
|
12
|
+
|
13
|
+
## Getting Started
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'activeinteractor'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
bundle
|
25
|
+
```
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
gem install activeinteractor
|
31
|
+
```
|
32
|
+
|
33
|
+
If you're working with a rails project you will also want to run:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
rails generate active_interactor:install
|
37
|
+
```
|
38
|
+
|
39
|
+
This will create an initializer and a new class called `ApplicationInteractor`
|
40
|
+
at `app/interactors/application_interactor.rb`
|
41
|
+
|
42
|
+
you can then automatically generate interactors and interactor organizers with:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
rails generate interactor MyInteractor
|
46
|
+
```
|
47
|
+
|
48
|
+
```bash
|
49
|
+
rails generate interactor:organizer MyInteractor1 MyInteractor2
|
50
|
+
```
|
51
|
+
|
52
|
+
These two generators will automatically create an interactor class which
|
53
|
+
inherits from `ApplicationInteractor` and a matching spec or test file.
|
54
|
+
|
55
|
+
## What is an Interactor
|
56
|
+
|
57
|
+
An interactor is a simple, single-purpose service object.
|
58
|
+
|
59
|
+
Interactors can be used to reduce the responsibility of your controllers,
|
60
|
+
workers, and models and encapsulate your application's [business logic][business_logic_wikipedia].
|
61
|
+
Each interactor represents one thing that your application does.
|
62
|
+
|
63
|
+
## Usage
|
64
|
+
|
65
|
+
### Context
|
66
|
+
|
67
|
+
Each interactor will have it's own immutable `context` and `context` class.
|
68
|
+
For example:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class MyInteractor < ActiveInteractor::Base
|
72
|
+
end
|
73
|
+
|
74
|
+
MyInteractor.context_class #=> MyInteractor::Context
|
75
|
+
```
|
76
|
+
|
77
|
+
An interactor's context contains everything the interactor needs to do its work.
|
78
|
+
When an interactor does its single purpose, it affects its given context.
|
79
|
+
|
80
|
+
#### Adding to the Context
|
81
|
+
|
82
|
+
All instances of `context` inherit from `OpenStruct`. As an interactor runs it can
|
83
|
+
add information to it's `context`.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
context.user = user
|
87
|
+
```
|
88
|
+
|
89
|
+
#### Failing the Context
|
90
|
+
|
91
|
+
When something goes wrong in your interactor, you can flag the context as failed.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
context.fail!
|
95
|
+
```
|
96
|
+
|
97
|
+
When given a hash argument or an instance of `ActiveModel::Errors`, the fail!
|
98
|
+
method can also update the context. The following are equivalent:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
context.errors.merge!(user.errors)
|
102
|
+
context.fail!
|
103
|
+
```
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
context.fail!(user.errors)
|
107
|
+
```
|
108
|
+
|
109
|
+
You can ask a context if it's a failure:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
context.failure? #=> false
|
113
|
+
context.fail!
|
114
|
+
context.failure? #=> true
|
115
|
+
```
|
116
|
+
|
117
|
+
or if it's a success:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
context.success? # => true
|
121
|
+
context.fail!
|
122
|
+
context.success? # => false
|
123
|
+
```
|
124
|
+
|
125
|
+
#### Dealing with Failure
|
126
|
+
|
127
|
+
`context.fail!` always throws an exception of type `ActiveInteractor::Context::Failure`.
|
128
|
+
|
129
|
+
Normally, however, these exceptions are not seen. In the recommended usage, the consuming
|
130
|
+
object invokes the interactor using the class method call, then checks the `success?` method of
|
131
|
+
the context.
|
132
|
+
|
133
|
+
This works because the call class method swallows exceptions. When unit testing an interactor, if calling
|
134
|
+
custom business logic methods directly and bypassing call, be aware that `fail!` will generate such exceptions.
|
135
|
+
|
136
|
+
See [Using Interactors](#using-interactors), below, for the recommended usage of `perform` and `success?`.
|
137
|
+
|
138
|
+
#### Context Attributes
|
139
|
+
|
140
|
+
Each `context` instance have basic attribute assignment methods which can be invoked directly
|
141
|
+
from the interactor. You never need to directly interface with an interactor's context class.
|
142
|
+
Assigning attributes to a `context` is a simple way to explicitly defined what properties a
|
143
|
+
`context` should have after an interactor has done it's work.
|
144
|
+
|
145
|
+
You can see what attributes are defined on a given `context` with the `#attributes` method:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class MyInteractor < ActiveInteractor::Base
|
149
|
+
# we define user as an attribute because it will be assigned a value
|
150
|
+
# in the perform method.
|
151
|
+
context_attributes :first_name, :last_name, :email, :user
|
152
|
+
end
|
153
|
+
|
154
|
+
context = MyInteractor.perform(
|
155
|
+
first_name: 'Aaron',
|
156
|
+
last_name: 'Allen',
|
157
|
+
email: 'hello@aaronmallen.me',
|
158
|
+
occupation: 'Software Dude'
|
159
|
+
)
|
160
|
+
#=> <#<MyInteractor::Context first_name='Aaron', last_name='Allen, email='hello@aaronmallen.me', occupation='Software Dude'>
|
161
|
+
|
162
|
+
context.attributes #=> { first_name: 'Aaron', last_name: 'Allen', email: 'hello@aaronmallen.me' }
|
163
|
+
context.occupation #=> 'Software Dude'
|
164
|
+
```
|
165
|
+
|
166
|
+
You can see what properties are defined on a given `context` with the `#keys` method
|
167
|
+
regardless of whether or not the properties are defined in a `context#attributes`:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
context.keys #=> [:first_name, :last_name, :email, :occupation]
|
171
|
+
```
|
172
|
+
|
173
|
+
Finally you can invoke `#clean!` on a context to remove any properties not explicitly
|
174
|
+
defined in a `context#attributes`:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
context.clean! #=> { occupation: 'Software Dude' }
|
178
|
+
context.occupation #=> nil
|
179
|
+
```
|
180
|
+
|
181
|
+
#### Validating the Context
|
182
|
+
|
183
|
+
`ActiveInteractor` delegates all the validation methods provided by [ActiveModel::Validations]
|
184
|
+
onto an interactor's context class from the interactor itself. All of the methods found in
|
185
|
+
[ActiveModel::Validations] can be invoked directly on your interactor with the prefix `context_`.
|
186
|
+
|
187
|
+
`ActiveInteractor` provides two validation callback steps:
|
188
|
+
|
189
|
+
* `:calling` used before `#perform` is invoked
|
190
|
+
* `:called` used after `#perform` is invoked
|
191
|
+
|
192
|
+
A basic implementation might look like this:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class MyInteractor < ActiveInteractor::Base
|
196
|
+
context_attributes :first_name, :last_name, :email, :user
|
197
|
+
# only validates presence before perform is invoked
|
198
|
+
context_validates :first_name, presence: true, on: :calling
|
199
|
+
# validates before and after perform is invoked
|
200
|
+
context_validates :email, presence: true,
|
201
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
202
|
+
# validates after perform is invoked
|
203
|
+
context_validates :user, presence: true, on: :called
|
204
|
+
context_validate :user_is_a_user, on: :called
|
205
|
+
|
206
|
+
def perform
|
207
|
+
context.user = User.create_with(
|
208
|
+
first_name: context.first_name,
|
209
|
+
last_name: context.last_name
|
210
|
+
).find_or_create_by(email: context.email)
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def user_is_a_user
|
216
|
+
return if context.user.is_a?(User)
|
217
|
+
|
218
|
+
context.errors.add(:user, :invalid)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context = MyInteractor.perform(last_name: 'Allen')
|
223
|
+
#=> <#MyInteractor::Context last_name='Allen>
|
224
|
+
context.failure? #=> true
|
225
|
+
context.valid? #=> false
|
226
|
+
context.errors[:first_name] #=> ['can not be blank']
|
227
|
+
|
228
|
+
context = MyInterator.perform(first_name: 'Aaron', email: 'hello@aaronmallen.me')
|
229
|
+
#=> <#MyInteractor::Context first_name='Aaron', email='hello@aaronmallen.me'>
|
230
|
+
context.success? #=> true
|
231
|
+
context.valid? #=> true
|
232
|
+
context.errors.empty? #=> true
|
233
|
+
```
|
234
|
+
|
235
|
+
### Callbacks
|
236
|
+
|
237
|
+
`ActiveInteractor` uses [ActiveModel::Callbacks] and [ActiveModel::Validations::Callbacks]
|
238
|
+
on context validation, `perform`, and `rollback`. Callbacks can be defined with a `block`,
|
239
|
+
`Proc`, or `Symbol` method name and take the same conditional arguments outlined
|
240
|
+
in those two modules.
|
241
|
+
|
242
|
+
**NOTE:** When using symbolized method names as arguments the context class
|
243
|
+
will first attempt to invoke the method on itself, if it cannot find the defined
|
244
|
+
method it will attempt to invoke it on the interactor. Be concious of scope
|
245
|
+
when defining these methods.
|
246
|
+
|
247
|
+
#### Validation Callbacks
|
248
|
+
|
249
|
+
We can do work before an interactor's context is validated with the `before_context_validation` method:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class MyInteractor < ActiveInteractor::Base
|
253
|
+
context_attributes :first_name, :last_name, :email, :user
|
254
|
+
context_validates :last_name, presence: true
|
255
|
+
before_context_validation { last_name ||= 'Unknown' }
|
256
|
+
end
|
257
|
+
|
258
|
+
context = MyInteractor.perform(first_name: 'Aaron', email: 'hello@aaronmallen.me')
|
259
|
+
context.valid? #=> true
|
260
|
+
context.last_name #=> 'Unknown'
|
261
|
+
```
|
262
|
+
|
263
|
+
We can do work after an interactor's context is validated with the `after_context_validation` method:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class MyInteractor < ActiveInteractor::Base
|
267
|
+
context_attributes :first_name, :last_name, :email, :user
|
268
|
+
context_validates :email, presence: true,
|
269
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
270
|
+
after_context_validation :downcase_email!
|
271
|
+
|
272
|
+
private
|
273
|
+
|
274
|
+
def downcase_email
|
275
|
+
context.email = context.email&.downcase!
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
context = MyInteractor.perform(first_name: 'Aaron', email: 'HELLO@aaronmallen.me')
|
280
|
+
context.email #=> 'hello@aaronmallen.me'
|
281
|
+
```
|
282
|
+
|
283
|
+
We can prevent a context from failing when invalid by invoking the
|
284
|
+
`allow_context_to_be_invalid` class method:
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
class MyInteractor < ActiveInteractor::Base
|
288
|
+
allow_context_to_be_invalid
|
289
|
+
context_attributes :first_name, :last_name, :email, :user
|
290
|
+
context_validates :first_name, presence: true
|
291
|
+
end
|
292
|
+
|
293
|
+
context = MyInteractor.perform(email: 'HELLO@aaronmallen.me')
|
294
|
+
context.valid? #=> false
|
295
|
+
context.success? #=> true
|
296
|
+
```
|
297
|
+
|
298
|
+
#### Context Attribute Callbacks
|
299
|
+
|
300
|
+
We can ensure only properties in the context's `attributes` are
|
301
|
+
returned after `perform` is invoked with the `clean_context_on_completion`
|
302
|
+
class method:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
class MyInteractor < ActiveInteractor::Base
|
306
|
+
clean_context_on_completion
|
307
|
+
context_attributes :user
|
308
|
+
|
309
|
+
def perform
|
310
|
+
context.user = User.create_with(
|
311
|
+
occupation: context.occupation
|
312
|
+
).find_or_create_by(email: context.email)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
context = MyInteractor.perform(email: 'hello@aaronmallen.me', occupation: 'Software Dude')
|
317
|
+
context.email #=> nil
|
318
|
+
context.occupation #=> nil
|
319
|
+
context.user #=> <#User email='hello@aaronmallen.me', occupation='Software Dude'>
|
320
|
+
```
|
321
|
+
|
322
|
+
#### Perform Callbacks
|
323
|
+
|
324
|
+
We can do work before `perform` is invoked with the `before_perform` method:
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
class MyInteractor < ActiveInteractor::Base
|
328
|
+
before_perform :print_start
|
329
|
+
|
330
|
+
def perform
|
331
|
+
puts 'Performing'
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
def print_start
|
337
|
+
puts 'Start'
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
context = MyInteractor.perform
|
342
|
+
"Start"
|
343
|
+
"Performing"
|
344
|
+
```
|
345
|
+
|
346
|
+
We can do work around `perform` invokation with the `around_perform` method:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
class MyInteractor < ActiveInteractor::Base
|
350
|
+
context_validates :first_name, presence: true
|
351
|
+
around_perform :track_time, if: :context_valid?
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def track_time
|
356
|
+
context.start_time = Time.now.utc
|
357
|
+
yield
|
358
|
+
context.end_time = Time.now.utc
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
context = MyInteractor.perform(first_name: 'Aaron')
|
363
|
+
context.start_time #=> 2019-01-01 00:00:00 UTC
|
364
|
+
context.end_time # #=> 2019-01-01 00:00:01 UTC
|
365
|
+
|
366
|
+
context = MyInteractor.perform
|
367
|
+
context.valid? #=> false
|
368
|
+
context.start_time #=> nil
|
369
|
+
context.end_time # #=> nil
|
370
|
+
```
|
371
|
+
|
372
|
+
We can do work after `perform` is invoked with the `after_perform` method:
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
class MyInteractor < ActiveInteractor::Base
|
376
|
+
after_perform :print_done
|
377
|
+
|
378
|
+
def perform
|
379
|
+
puts 'Performing'
|
380
|
+
end
|
381
|
+
|
382
|
+
private
|
383
|
+
|
384
|
+
def print_done
|
385
|
+
puts 'Done'
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
context = MyInteractor.perform
|
390
|
+
"Performing"
|
391
|
+
"Done"
|
392
|
+
```
|
393
|
+
|
394
|
+
#### Rollback Callbacks
|
395
|
+
|
396
|
+
We can do work before `rollback` is invoked with the `before_rollback` method:
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
class MyInteractor < ActiveInteractor::Base
|
400
|
+
before_rollback :print_start
|
401
|
+
|
402
|
+
def rollback
|
403
|
+
puts 'Rolling Back'
|
404
|
+
end
|
405
|
+
|
406
|
+
private
|
407
|
+
|
408
|
+
def print_start
|
409
|
+
puts 'Start'
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
context = MyInteractor.perform
|
414
|
+
context.rollback!
|
415
|
+
"Start"
|
416
|
+
"Rolling Back"
|
417
|
+
```
|
418
|
+
|
419
|
+
We can do work around `rollback` invokation with the `around_rollback` method:
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
class MyInteractor < ActiveInteractor::Base
|
423
|
+
around_rollback :track_time
|
424
|
+
|
425
|
+
private
|
426
|
+
|
427
|
+
def track_time
|
428
|
+
context.start_time = Time.now.utc
|
429
|
+
yield
|
430
|
+
context.end_time = Time.now.utc
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
context = MyInteractor.perform
|
435
|
+
context.rollback!
|
436
|
+
context.start_time #=> 2019-01-01 00:00:00 UTC
|
437
|
+
context.end_time # #=> 2019-01-01 00:00:01 UTC
|
438
|
+
```
|
439
|
+
|
440
|
+
We can do work after `rollback` is invoked with the `after_rollback` method:
|
441
|
+
|
442
|
+
```ruby
|
443
|
+
class MyInteractor < ActiveInteractor::Base
|
444
|
+
after_rollback :print_done
|
445
|
+
|
446
|
+
def rollback
|
447
|
+
puts 'Rolling Back'
|
448
|
+
end
|
449
|
+
|
450
|
+
private
|
451
|
+
|
452
|
+
def print_done
|
453
|
+
puts 'Done'
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
context = MyInteractor.perform
|
458
|
+
context.rollback!
|
459
|
+
"Rolling Back"
|
460
|
+
"Done"
|
461
|
+
```
|
462
|
+
|
463
|
+
### Using Interactors
|
464
|
+
|
465
|
+
Most of the time, your application will use its interactors from its controllers. The following controller:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
class SessionsController < ApplicationController
|
469
|
+
def create
|
470
|
+
if user = User.authenticate(session_params[:email], session_params[:password])
|
471
|
+
session[:user_token] = user.secret_token
|
472
|
+
redirect_to user
|
473
|
+
else
|
474
|
+
flash.now[:message] = "Please try again."
|
475
|
+
render :new
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
private
|
480
|
+
|
481
|
+
def session_params
|
482
|
+
params.require(:session).permit(:email, :password)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
```
|
486
|
+
|
487
|
+
can be refactored to:
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
class SessionsController < ApplicationController
|
491
|
+
def create
|
492
|
+
result = AuthenticateUser.perform(session_params)
|
493
|
+
|
494
|
+
if result.success?
|
495
|
+
session[:user_token] = result.token
|
496
|
+
redirect_to result.user
|
497
|
+
else
|
498
|
+
flash.now[:message] = t(result.errors.full_messages)
|
499
|
+
render :new
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
private
|
504
|
+
|
505
|
+
def session_params
|
506
|
+
params.require(:session).permit(:email, :password)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
given the basic interactor:
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
class AuthenticateUser < ActiveInteractor::Base
|
515
|
+
context_attributes :email, :password, :user, :token
|
516
|
+
context_validates :email, presence: true,
|
517
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
518
|
+
context_validates :password, presence: true
|
519
|
+
context_validates :user, presence: true, on: :called
|
520
|
+
|
521
|
+
def perform
|
522
|
+
context.user = User.authenticate(
|
523
|
+
context.email,
|
524
|
+
context.password
|
525
|
+
)
|
526
|
+
context.token = context.user.secret_token
|
527
|
+
end
|
528
|
+
end
|
529
|
+
```
|
530
|
+
|
531
|
+
The `perform` class method is the proper way to invoke an interactor.
|
532
|
+
The hash argument is converted to the interactor instance's context.
|
533
|
+
The `preform` instance method is invoked along with any callbacks and validations
|
534
|
+
that the interactor might define. Finally, the context (along with any changes made to it)
|
535
|
+
is returned.
|
536
|
+
|
537
|
+
### Kinds of Interactors
|
538
|
+
|
539
|
+
There are two kinds of interactors built into the Interactor library: basic interactors and organizers.
|
540
|
+
|
541
|
+
#### Interactors
|
542
|
+
|
543
|
+
A basic interactor is a class that includes Interactor and defines call.
|
544
|
+
|
545
|
+
```ruby
|
546
|
+
class AuthenticateUser
|
547
|
+
include Interactor
|
548
|
+
|
549
|
+
def perform
|
550
|
+
if user = User.authenticate(context.email, context.password)
|
551
|
+
context.user = user
|
552
|
+
context.token = user.secret_token
|
553
|
+
else
|
554
|
+
context.fail!
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
```
|
559
|
+
|
560
|
+
Basic interactors are the building blocks. They are your application's single-purpose units of work.
|
561
|
+
|
562
|
+
#### Organizers
|
563
|
+
|
564
|
+
An organizer is an important variation on the basic interactor. Its single purpose is to run other interactors.
|
565
|
+
|
566
|
+
```ruby
|
567
|
+
class PlaceOrder
|
568
|
+
include Interactor::Organizer
|
569
|
+
|
570
|
+
organize CreateOrder, ChargeCard, SendThankYou
|
571
|
+
end
|
572
|
+
```
|
573
|
+
|
574
|
+
In the controller, you can run the `PlaceOrder` organizer just like you would any other interactor:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
class OrdersController < ApplicationController
|
578
|
+
def create
|
579
|
+
result = PlaceOrder.call(order_params: order_params)
|
580
|
+
|
581
|
+
if result.success?
|
582
|
+
redirect_to result.order
|
583
|
+
else
|
584
|
+
@order = result.order
|
585
|
+
render :new
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
private
|
590
|
+
|
591
|
+
def order_params
|
592
|
+
params.require(:order).permit!
|
593
|
+
end
|
594
|
+
end
|
595
|
+
```
|
596
|
+
|
597
|
+
The organizer passes its context to the interactors that it organizes, one at a time and in order.
|
598
|
+
Each interactor may change that context before it's passed along to the next interactor.
|
599
|
+
|
600
|
+
#### Rollback
|
601
|
+
|
602
|
+
If any one of the organized interactors fails its context, the organizer stops.
|
603
|
+
If the `ChargeCard` interactor fails, `SendThankYou` is never called.
|
604
|
+
|
605
|
+
In addition, any interactors that had already run are given the chance to undo themselves, in reverse order.
|
606
|
+
Simply define the rollback method on your interactors:
|
607
|
+
|
608
|
+
```ruby
|
609
|
+
class CreateOrder
|
610
|
+
include Interactor
|
611
|
+
|
612
|
+
def perform
|
613
|
+
order = Order.create(order_params)
|
614
|
+
|
615
|
+
if order.persisted?
|
616
|
+
context.order = order
|
617
|
+
else
|
618
|
+
context.fail!
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def rollback
|
623
|
+
context.order.destroy
|
624
|
+
end
|
625
|
+
end
|
626
|
+
```
|
627
|
+
|
628
|
+
## Development
|
629
|
+
|
630
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
631
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
632
|
+
|
633
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
634
|
+
|
635
|
+
Additionally you can run tests in both rails 2.5 and rails 2.6 with `bin/test`.
|
636
|
+
|
637
|
+
## Contributing
|
638
|
+
|
639
|
+
Read our guidelines for [Contributing](CONTRIBUTING.md).
|
640
|
+
|
641
|
+
## Acknowledgements
|
642
|
+
|
643
|
+
* Special thanks to [@collectiveidea] for their amazing foundational work on
|
644
|
+
the [interactor][collective_idea_interactors] gem.
|
645
|
+
* Special thanks to the [@rails] team for their work on [ActiveModel][active_model_git]
|
646
|
+
and [ActiveSupport][active_support_git] gems.
|
647
|
+
|
648
|
+
## License
|
649
|
+
|
650
|
+
The gem is available as open source under the terms of the [MIT License][mit_license].
|
651
|
+
|
652
|
+
[ActiveModel::Callbacks]: https://api.rubyonrails.org/classes/ActiveModel/Callbacks.html
|
653
|
+
[ActiveModel::Validations]: https://api.rubyonrails.org/classes/ActiveModel/Validations.html
|
654
|
+
[ActiveModel::Validations::Callbacks]: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks.html
|
655
|
+
[collective_idea_interactors]: https://github.com/collectiveidea/interactor
|
656
|
+
[business_logic_wikipedia]: https://en.wikipedia.org/wiki/Business_logic
|
657
|
+
[@collectiveidea]: https://github.com/collectiveidea
|
658
|
+
[@rails]: https://github.com/rails
|
659
|
+
[active_model_git]: https://github.com/rails/rails/tree/master/activemodel
|
660
|
+
[active_support_git]: https://github.com/rails/rails/tree/master/activesupport
|
661
|
+
[mit_license]: https://opensource.org/licenses/MIT
|