interactor_with_steroids 0.0.1

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.
data/README.md ADDED
@@ -0,0 +1,602 @@
1
+ # Interactor
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/interactor.svg)](http://rubygems.org/gems/interactor)
4
+ [![Build Status](https://img.shields.io/travis/collectiveidea/interactor/master.svg)](https://travis-ci.org/collectiveidea/interactor)
5
+ [![Maintainability](https://img.shields.io/codeclimate/maintainability/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)
6
+ [![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)
7
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
8
+
9
+ ## Getting Started
10
+
11
+ Add Interactor to your Gemfile and `bundle install`.
12
+
13
+ ```ruby
14
+ gem "interactor", "~> 3.0"
15
+ ```
16
+
17
+ ## What is an Interactor?
18
+
19
+ An interactor is a simple, single-purpose object.
20
+
21
+ Interactors are used to encapsulate your application's
22
+ [business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor
23
+ represents one thing that your application *does*.
24
+
25
+ ### Context
26
+
27
+ An interactor is given a *context*. The context contains everything the
28
+ interactor needs to do its work.
29
+
30
+ When an interactor does its single purpose, it affects its given context.
31
+
32
+ #### Adding to the Context
33
+
34
+ As an interactor runs it can add information to the context.
35
+
36
+ ```ruby
37
+ context.user = user
38
+ ```
39
+
40
+ #### Failing the Context
41
+
42
+ When something goes wrong in your interactor, you can flag the context as
43
+ failed.
44
+
45
+ ```ruby
46
+ context.fail!
47
+ ```
48
+
49
+ When given a hash argument, the `fail!` method can also update the context. The
50
+ following are equivalent:
51
+
52
+ ```ruby
53
+ context.error = "Boom!"
54
+ context.fail!
55
+ ```
56
+
57
+ ```ruby
58
+ context.fail!(error: "Boom!")
59
+ ```
60
+
61
+ You can ask a context if it's a failure:
62
+
63
+ ```ruby
64
+ context.failure? # => false
65
+ context.fail!
66
+ context.failure? # => true
67
+ ```
68
+
69
+ or if it's a success.
70
+
71
+ ```ruby
72
+ context.success? # => true
73
+ context.fail!
74
+ context.success? # => false
75
+ ```
76
+
77
+ #### Dealing with Failure
78
+
79
+ `context.fail!` always throws an exception of type `Interactor::Failure`.
80
+
81
+ Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interactor using the class method `call`, then checks the `success?` method of the context.
82
+
83
+ This works because the `call` class method swallows exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
84
+
85
+ See *Interactors in the Controller*, below, for the recommended usage of `call` and `success?`.
86
+
87
+ ### Hooks
88
+
89
+ #### Before Hooks
90
+
91
+ Sometimes an interactor needs to prepare its context before the interactor is
92
+ even run. This can be done with before hooks on the interactor.
93
+
94
+ ```ruby
95
+ before do
96
+ context.emails_sent = 0
97
+ end
98
+ ```
99
+
100
+ A symbol argument can also be given, rather than a block.
101
+
102
+ ```ruby
103
+ before :zero_emails_sent
104
+
105
+ def zero_emails_sent
106
+ context.emails_sent = 0
107
+ end
108
+ ```
109
+
110
+ #### After Hooks
111
+
112
+ Interactors can also perform teardown operations after the interactor instance
113
+ is run.
114
+
115
+ ```ruby
116
+ after do
117
+ context.user.reload
118
+ end
119
+ ```
120
+
121
+ NB: After hooks are only run on success. If the `fail!` method is called, the interactor's after hooks are not run.
122
+
123
+ #### Around Hooks
124
+
125
+ You can also define around hooks in the same way as before or after hooks, using
126
+ either a block or a symbol method name. The difference is that an around block
127
+ or method accepts a single argument. Invoking the `call` method on that argument
128
+ will continue invocation of the interactor. For example, with a block:
129
+
130
+ ```ruby
131
+ around do |interactor|
132
+ context.start_time = Time.now
133
+ interactor.call
134
+ context.finish_time = Time.now
135
+ end
136
+ ```
137
+
138
+ With a method:
139
+
140
+ ```ruby
141
+ around :time_execution
142
+
143
+ def time_execution(interactor)
144
+ context.start_time = Time.now
145
+ interactor.call
146
+ context.finish_time = Time.now
147
+ end
148
+ ```
149
+
150
+ NB: If the `fail!` method is called, all of the interactor's around hooks cease execution, and no code after `interactor.call` will be run.
151
+
152
+ #### Hook Sequence
153
+
154
+ Before hooks are invoked in the order in which they were defined while after
155
+ hooks are invoked in the opposite order. Around hooks are invoked outside of any
156
+ defined before and after hooks. For example:
157
+
158
+ ```ruby
159
+ around do |interactor|
160
+ puts "around before 1"
161
+ interactor.call
162
+ puts "around after 1"
163
+ end
164
+
165
+ around do |interactor|
166
+ puts "around before 2"
167
+ interactor.call
168
+ puts "around after 2"
169
+ end
170
+
171
+ before do
172
+ puts "before 1"
173
+ end
174
+
175
+ before do
176
+ puts "before 2"
177
+ end
178
+
179
+ after do
180
+ puts "after 1"
181
+ end
182
+
183
+ after do
184
+ puts "after 2"
185
+ end
186
+ ```
187
+
188
+ will output:
189
+
190
+ ```
191
+ around before 1
192
+ around before 2
193
+ before 1
194
+ before 2
195
+ after 2
196
+ after 1
197
+ around after 2
198
+ around after 1
199
+ ```
200
+
201
+ #### Interactor Concerns
202
+
203
+ An interactor can define multiple before/after hooks, allowing common hooks to
204
+ be extracted into interactor concerns.
205
+
206
+ ```ruby
207
+ module InteractorTimer
208
+ extend ActiveSupport::Concern
209
+
210
+ included do
211
+ around do |interactor|
212
+ context.start_time = Time.now
213
+ interactor.call
214
+ context.finish_time = Time.now
215
+ end
216
+ end
217
+ end
218
+ ```
219
+
220
+ ### An Example Interactor
221
+
222
+ Your application could use an interactor to authenticate a user.
223
+
224
+ ```ruby
225
+ class AuthenticateUser
226
+ include Interactor
227
+
228
+ def call
229
+ if user = User.authenticate(context.email, context.password)
230
+ context.user = user
231
+ context.token = user.secret_token
232
+ else
233
+ context.fail!(message: "authenticate_user.failure")
234
+ end
235
+ end
236
+ end
237
+ ```
238
+
239
+ To define an interactor, simply create a class that includes the `Interactor`
240
+ module and give it a `call` instance method. The interactor can access its
241
+ `context` from within `call`.
242
+
243
+ ## Interactors in the Controller
244
+
245
+ Most of the time, your application will use its interactors from its
246
+ controllers. The following controller:
247
+
248
+ ```ruby
249
+ class SessionsController < ApplicationController
250
+ def create
251
+ if user = User.authenticate(session_params[:email], session_params[:password])
252
+ session[:user_token] = user.secret_token
253
+ redirect_to user
254
+ else
255
+ flash.now[:message] = "Please try again."
256
+ render :new
257
+ end
258
+ end
259
+
260
+ private
261
+
262
+ def session_params
263
+ params.require(:session).permit(:email, :password)
264
+ end
265
+ end
266
+ ```
267
+
268
+ can be refactored to:
269
+
270
+ ```ruby
271
+ class SessionsController < ApplicationController
272
+ def create
273
+ result = AuthenticateUser.call(session_params)
274
+
275
+ if result.success?
276
+ session[:user_token] = result.token
277
+ redirect_to result.user
278
+ else
279
+ flash.now[:message] = t(result.message)
280
+ render :new
281
+ end
282
+ end
283
+
284
+ private
285
+
286
+ def session_params
287
+ params.require(:session).permit(:email, :password)
288
+ end
289
+ end
290
+ ```
291
+
292
+ The `call` class method is the proper way to invoke an interactor. The hash
293
+ argument is converted to the interactor instance's context. The `call` instance
294
+ method is invoked along with any hooks that the interactor might define.
295
+ Finally, the context (along with any changes made to it) is returned.
296
+
297
+ ## When to Use an Interactor
298
+
299
+ Given the user authentication example, your controller may look like:
300
+
301
+ ```ruby
302
+ class SessionsController < ApplicationController
303
+ def create
304
+ result = AuthenticateUser.call(session_params)
305
+
306
+ if result.success?
307
+ session[:user_token] = result.token
308
+ redirect_to result.user
309
+ else
310
+ flash.now[:message] = t(result.message)
311
+ render :new
312
+ end
313
+ end
314
+
315
+ private
316
+
317
+ def session_params
318
+ params.require(:session).permit(:email, :password)
319
+ end
320
+ end
321
+ ```
322
+
323
+ For such a simple use case, using an interactor can actually require *more*
324
+ code. So why use an interactor?
325
+
326
+ ### Clarity
327
+
328
+ [We](http://collectiveidea.com) often use interactors right off the bat for all
329
+ of our destructive actions (`POST`, `PUT` and `DELETE` requests) and since we
330
+ put our interactors in `app/interactors`, a glance at that directory gives any
331
+ developer a quick understanding of everything the application *does*.
332
+
333
+ ```
334
+ ▾ app/
335
+ ▸ controllers/
336
+ ▸ helpers/
337
+ ▾ interactors/
338
+ authenticate_user.rb
339
+ cancel_account.rb
340
+ publish_post.rb
341
+ register_user.rb
342
+ remove_post.rb
343
+ ▸ mailers/
344
+ ▸ models/
345
+ ▸ views/
346
+ ```
347
+
348
+ **TIP:** Name your interactors after your business logic, not your
349
+ implementation. `CancelAccount` will serve you better than `DestroyUser` as the
350
+ account cancellation interaction takes on more responsibility in the future.
351
+
352
+ ### The Future™
353
+
354
+ **SPOILER ALERT:** Your use case won't *stay* so simple.
355
+
356
+ In [our](http://collectiveidea.com) experience, a simple task like
357
+ authenticating a user will eventually take on multiple responsibilities:
358
+
359
+ * Welcoming back a user who hadn't logged in for a while
360
+ * Prompting a user to update his or her password
361
+ * Locking out a user in the case of too many failed attempts
362
+ * Sending the lock-out email notification
363
+
364
+ The list goes on, and as that list grows, so does your controller. This is how
365
+ fat controllers are born.
366
+
367
+ If instead you use an interactor right away, as responsibilities are added, your
368
+ controller (and its tests) change very little or not at all. Choosing the right
369
+ kind of interactor can also prevent simply shifting those added responsibilities
370
+ to the interactor.
371
+
372
+ ## Testing Interactors
373
+
374
+ When written correctly, an interactor is easy to test because it only *does* one
375
+ thing. Take the following interactor:
376
+
377
+ ```ruby
378
+ class AuthenticateUser
379
+ include Interactor
380
+
381
+ def call
382
+ if user = User.authenticate(context.email, context.password)
383
+ context.user = user
384
+ context.token = user.secret_token
385
+ else
386
+ context.fail!(message: "authenticate_user.failure")
387
+ end
388
+ end
389
+ end
390
+ ```
391
+
392
+ You can test just this interactor's single purpose and how it affects the
393
+ context.
394
+
395
+ ```ruby
396
+ describe AuthenticateUser do
397
+ subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") }
398
+
399
+ describe ".call" do
400
+ context "when given valid credentials" do
401
+ let(:user) { double(:user, secret_token: "token") }
402
+
403
+ before do
404
+ allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user)
405
+ end
406
+
407
+ it "succeeds" do
408
+ expect(context).to be_a_success
409
+ end
410
+
411
+ it "provides the user" do
412
+ expect(context.user).to eq(user)
413
+ end
414
+
415
+ it "provides the user's secret token" do
416
+ expect(context.token).to eq("token")
417
+ end
418
+ end
419
+
420
+ context "when given invalid credentials" do
421
+ before do
422
+ allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil)
423
+ end
424
+
425
+ it "fails" do
426
+ expect(context).to be_a_failure
427
+ end
428
+
429
+ it "provides a failure message" do
430
+ expect(context.message).to be_present
431
+ end
432
+ end
433
+ end
434
+ end
435
+ ```
436
+
437
+ [We](http://collectiveidea.com) use RSpec but the same approach applies to any
438
+ testing framework.
439
+
440
+ ### Isolation
441
+
442
+ You may notice that we stub `User.authenticate` in our test rather than creating
443
+ users in the database. That's because our purpose in
444
+ `spec/interactors/authenticate_user_spec.rb` is to test just the
445
+ `AuthenticateUser` interactor. The `User.authenticate` method is put through its
446
+ own paces in `spec/models/user_spec.rb`.
447
+
448
+ It's a good idea to define your own interfaces to your models. Doing so makes it
449
+ easy to draw a line between which responsibilities belong to the interactor and
450
+ which to the model. The `User.authenticate` method is a good, clear line.
451
+ Imagine the interactor otherwise:
452
+
453
+ ```ruby
454
+ class AuthenticateUser
455
+ include Interactor
456
+
457
+ def call
458
+ user = User.where(email: context.email).first
459
+
460
+ # Yuck!
461
+ if user && BCrypt::Password.new(user.password_digest) == context.password
462
+ context.user = user
463
+ else
464
+ context.fail!(message: "authenticate_user.failure")
465
+ end
466
+ end
467
+ end
468
+ ```
469
+
470
+ It would be very difficult to test this interactor in isolation and even if you
471
+ did, as soon as you change your ORM or your encryption algorithm (both model
472
+ concerns), your interactors (business concerns) break.
473
+
474
+ *Draw clear lines.*
475
+
476
+ ### Integration
477
+
478
+ While it's important to test your interactors in isolation, it's just as
479
+ important to write good integration or acceptance tests.
480
+
481
+ One of the pitfalls of testing in isolation is that when you stub a method, you
482
+ could be hiding the fact that the method is broken, has changed or doesn't even
483
+ exist.
484
+
485
+ When you write full-stack tests that tie all of the pieces together, you can be
486
+ sure that your application's individual pieces are working together as expected.
487
+ That becomes even more important when you add a new layer to your code like
488
+ interactors.
489
+
490
+ **TIP:** If you track your test coverage, try for 100% coverage *before*
491
+ integrations tests. Then keep writing integration tests until you sleep well at
492
+ night.
493
+
494
+ ### Controllers
495
+
496
+ One of the advantages of using interactors is how much they simplify controllers
497
+ and their tests. Because you're testing your interactors thoroughly in isolation
498
+ as well as in integration tests (right?), you can remove your business logic
499
+ from your controller tests.
500
+
501
+ ```ruby
502
+ class SessionsController < ApplicationController
503
+ def create
504
+ result = AuthenticateUser.call(session_params)
505
+
506
+ if result.success?
507
+ session[:user_token] = result.token
508
+ redirect_to result.user
509
+ else
510
+ flash.now[:message] = t(result.message)
511
+ render :new
512
+ end
513
+ end
514
+
515
+ private
516
+
517
+ def session_params
518
+ params.require(:session).permit(:email, :password)
519
+ end
520
+ end
521
+ ```
522
+
523
+ ```ruby
524
+ describe SessionsController do
525
+ describe "#create" do
526
+ before do
527
+ expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
528
+ end
529
+
530
+ context "when successful" do
531
+ let(:user) { double(:user, id: 1) }
532
+ let(:context) { double(:context, success?: true, user: user, token: "token") }
533
+
534
+ it "saves the user's secret token in the session" do
535
+ expect {
536
+ post :create, session: { email: "john@doe.com", password: "secret" }
537
+ }.to change {
538
+ session[:user_token]
539
+ }.from(nil).to("token")
540
+ end
541
+
542
+ it "redirects to the homepage" do
543
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
544
+
545
+ expect(response).to redirect_to(user_path(user))
546
+ end
547
+ end
548
+
549
+ context "when unsuccessful" do
550
+ let(:context) { double(:context, success?: false, message: "message") }
551
+
552
+ it "sets a flash message" do
553
+ expect {
554
+ post :create, session: { email: "john@doe.com", password: "secret" }
555
+ }.to change {
556
+ flash[:message]
557
+ }.from(nil).to(I18n.translate("message"))
558
+ end
559
+
560
+ it "renders the login form" do
561
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
562
+
563
+ expect(response).to render_template(:new)
564
+ end
565
+ end
566
+ end
567
+ end
568
+ ```
569
+
570
+ This controller test will have to change very little during the life of the
571
+ application because all of the magic happens in the interactor.
572
+
573
+ ### Rails
574
+
575
+ [We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We
576
+ put our interactors in `app/interactors` and we name them as verbs:
577
+
578
+ * `AddProductToCart`
579
+ * `AuthenticateUser`
580
+ * `PlaceOrder`
581
+ * `RegisterUser`
582
+ * `RemoveProductFromCart`
583
+
584
+ See: [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
585
+
586
+ ## Contributions
587
+
588
+ Interactor is open source and contributions from the community are encouraged!
589
+ No contribution is too small.
590
+
591
+ See Interactor's
592
+ [contribution guidelines](CONTRIBUTING.md) for more information.
593
+
594
+ ## Thank You
595
+
596
+ A very special thank you to [Attila Domokos](https://github.com/adomokos) for
597
+ his fantastic work on [LightService](https://github.com/adomokos/light-service).
598
+ Interactor is inspired heavily by the concepts put to code by Attila.
599
+
600
+ Interactor was born from a desire for a slightly simplified interface. We
601
+ understand that this is a matter of personal preference, so please take a look
602
+ at LightService as well!
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "standard/rake"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: [:spec, :standard]
@@ -0,0 +1,20 @@
1
+ require "English"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "interactor_with_steroids"
5
+ spec.version = "0.0.1"
6
+
7
+ spec.author = "Collective Idea/Sorare Team"
8
+ spec.email = "hello@sorare.com"
9
+ spec.description = "Interactor provides a common interface for performing complex user interactions."
10
+ spec.summary = "Simple interactor implementation"
11
+ spec.homepage = "https://github.com/sorare/interactor"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
+ spec.test_files = spec.files.grep(/^spec/)
16
+
17
+ spec.add_dependency "activesupport"
18
+ spec.add_development_dependency "bundler"
19
+ spec.add_development_dependency "rake"
20
+ end