interactor_with_steroids 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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