activeinteractor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![Version](https://img.shields.io/gem/v/activeinteractor.svg?logo=ruby&style=for-the-badge)](https://rubygems.org/gems/activeinteractor)
4
+ [![License](https://img.shields.io/github/license/aaronmallen/activeinteractor.svg?maxAge=300&style=for-the-badge)](https://github.com/aaronmallen/activeinteractor/blob/master/LICENSE)
5
+ [![Dependencies](https://img.shields.io/depfu/aaronmallen/activeinteractor.svg?maxAge=300&style=for-the-badge)](https://depfu.com/github/aaronmallen/activeinteractor)
6
+
7
+ [![Build Status](https://img.shields.io/travis/com/aaronmallen/activeinteractor/master.svg?logo=travis&maxAge=300&style=for-the-badge)](https://www.travis-ci.com/aaronmallen/activeinteractor)
8
+ [![Maintainability](https://img.shields.io/codeclimate/maintainability/aaronmallen/activeinteractor.svg?maxAge=300&style=for-the-badge)](https://codeclimate.com/github/aaronmallen/activeinteractor/maintainability)
9
+ [![Test Coverage](https://img.shields.io/codeclimate/coverage/aaronmallen/activeinteractor.svg?maxAge=300&style=for-the-badge)](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