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 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