├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .rspec ├── .standard.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── interactor.gemspec ├── lib ├── interactor.rb └── interactor │ ├── context.rb │ ├── error.rb │ ├── hooks.rb │ └── organizer.rb └── spec ├── integration_spec.rb ├── interactor ├── context_spec.rb ├── hooks_spec.rb └── organizer_spec.rb ├── interactor_spec.rb ├── spec_helper.rb └── support └── lint.rb /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | spec: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | ruby_version: 15 | - "3.1" 16 | - "3.2" 17 | - "3.3" 18 | - "3.4" 19 | - "head" 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby_version }} 28 | bundler-cache: true 29 | 30 | - name: Run Tests 31 | run: bundle exec rake 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 2.3.0 2 | ignore: 3 | - 'spec/**/*': 4 | - Lint/AmbiguousBlockAssociation 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.2.0 / Unreleased 2 | * [BUGFIX] Raise failures from nested contexts [#170] 3 | * [FEATURE] Add `ostruct` dependency to gemspec. 4 | * [FEATURE] Add support for Ruby 3 pattern matching on the Context [#200] 5 | 6 | ## 3.1.2 / 2019-12-29 7 | * [BUGFIX] Fix Context#fail! on Ruby 2.7 8 | 9 | ## 3.1.1 / 2018-05-30 10 | 11 | * [BUGFIX] Allow Context#fail! to accept a hash with string keys 12 | * [ENHANCEMENT] Many documentation updates 13 | 14 | ## 3.1.0 / 2014-10-13 15 | 16 | * [FEATURE] Add around hooks 17 | 18 | ## 3.0.1 / 2014-09-09 19 | 20 | * [ENHANCEMENT] Add TomDoc code documentation 21 | 22 | ## 3.0.0 / 2014-09-07 23 | 24 | * [FEATURE] Remove "magical" access to the context through the interactor 25 | * [FEATURE] Manage context values via setters/getters rather than hash access 26 | * [FEATURE] Change the primary interactor API method from "perform" to "call" 27 | * [FEATURE] Return the mutated context rather than the interactor instance 28 | * [FEATURE] Replace interactor setup with before and after hooks 29 | * [FEATURE] Abort execution immediately upon interactor failure 30 | * [ENHANCEMENT] Build a suite of realistic integration tests 31 | * [ENHANCEMENT] Move rollback responsibility into the context 32 | 33 | ## 2.1.1 / 2014-09-30 34 | 35 | * [FEATURE] Halt performance if the interactor fails prior 36 | * [ENHANCEMENT] Add support for Ruby 2.1 37 | 38 | ## 2.1.0 / 2013-09-05 39 | 40 | * [FEATURE] Roll back when an interactor within an organizer raises an error 41 | * [BUGFIX] Ensure that context-deferred methods respect string keys 42 | * [FEATURE] Respect context initialization from an indifferent access hash 43 | 44 | ## 2.0.1 / 2013-08-28 45 | 46 | * [BUGFIX] Allow YAML (de)serialization by fixing interactor allocation 47 | 48 | ## 2.0.0 / 2013-08-19 49 | 50 | * [BUGFIX] Fix rollback behavior within nested organizers 51 | * [BUGFIX] Skip rollback for the failed interactor 52 | 53 | ## 1.0.0 / 2013-08-17 54 | 55 | * Initial release! 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Interactor 2 | 3 | Interactor is open source and contributions from the community are encouraged! 4 | No contribution is too small. 5 | 6 | Please consider: 7 | 8 | * adding a feature 9 | * squashing a bug 10 | * writing [documentation](http://tomdoc.org) 11 | * reporting an issue 12 | * fixing a typo 13 | * correcting [style](https://github.com/styleguide/ruby) 14 | 15 | ## How do I contribute? 16 | 17 | For the best chance of having your changes merged, please: 18 | 19 | 1. [Fork](https://github.com/collectiveidea/interactor/fork) the project. 20 | 2. [Write](http://en.wikipedia.org/wiki/Test-driven_development) a failing test. 21 | 3. [Commit](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) changes that fix the tests. 22 | 4. [Submit](https://github.com/collectiveidea/interactor/pulls) a pull request with *at least* one animated GIF. 23 | 5. Be patient. 24 | 25 | ## Bug Reports 26 | 27 | If you are experiencing unexpected behavior and, after having read Interactor's 28 | documentation, are convinced this behavior is a bug, please: 29 | 30 | 1. [Search](https://github.com/collectiveidea/interactor/issues) existing issues. 31 | 2. Collect enough information to reproduce the issue: 32 | * Interactor version 33 | * Ruby version 34 | * Rails version (if applicable) 35 | * Specific setup conditions 36 | * Description of expected behavior 37 | * Description of actual behavior 38 | 3. [Submit](https://github.com/collectiveidea/interactor/issues/new) an issue. 39 | 4. Be patient. 40 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "standard" 6 | 7 | group :test do 8 | gem "codeclimate-test-reporter", require: false 9 | gem "rspec", "~> 3.7" 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Collective Idea 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactor 2 | 3 | [![Gem Version](https://img.shields.io/gem/v/interactor.svg)](http://rubygems.org/gems/interactor) 4 | [![Build Status](https://github.com/collectiveidea/interactor/actions/workflows/tests.yml/badge.svg)](https://github.com/collectiveidea/interactor/actions/workflows/tests.yml) 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 `Interactor::Failure` 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, status: :unprocessable_entity 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, status: :unprocessable_entity 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 | ## Kinds of Interactors 373 | 374 | There are two kinds of interactors built into the Interactor library: basic 375 | interactors and organizers. 376 | 377 | ### Interactors 378 | 379 | A basic interactor is a class that includes `Interactor` and defines `call`. 380 | 381 | ```ruby 382 | class AuthenticateUser 383 | include Interactor 384 | 385 | def call 386 | if user = User.authenticate(context.email, context.password) 387 | context.user = user 388 | context.token = user.secret_token 389 | else 390 | context.fail!(message: "authenticate_user.failure") 391 | end 392 | end 393 | end 394 | ``` 395 | 396 | Basic interactors are the building blocks. They are your application's 397 | single-purpose units of work. 398 | 399 | ### Organizers 400 | 401 | An organizer is an important variation on the basic interactor. Its single 402 | purpose is to run *other* interactors. 403 | 404 | ```ruby 405 | class PlaceOrder 406 | include Interactor::Organizer 407 | 408 | organize CreateOrder, ChargeCard, SendThankYou 409 | end 410 | ``` 411 | 412 | In the controller, you can run the `PlaceOrder` organizer just like you would 413 | any other interactor: 414 | 415 | ```ruby 416 | class OrdersController < ApplicationController 417 | def create 418 | result = PlaceOrder.call(order_params: order_params) 419 | 420 | if result.success? 421 | redirect_to result.order 422 | else 423 | @order = result.order 424 | render :new 425 | end 426 | end 427 | 428 | private 429 | 430 | def order_params 431 | params.require(:order).permit! 432 | end 433 | end 434 | ``` 435 | 436 | The organizer passes its context to the interactors that it organizes, one at a 437 | time and in order. Each interactor may change that context before it's passed 438 | along to the next interactor. 439 | 440 | #### Rollback 441 | 442 | If any one of the organized interactors fails its context, the organizer stops. 443 | If the `ChargeCard` interactor fails, `SendThankYou` is never called. 444 | 445 | In addition, any interactors that had already run are given the chance to undo 446 | themselves, in reverse order. Simply define the `rollback` method on your 447 | interactors: 448 | 449 | ```ruby 450 | class CreateOrder 451 | include Interactor 452 | 453 | def call 454 | order = Order.create(order_params) 455 | 456 | if order.persisted? 457 | context.order = order 458 | else 459 | context.fail! 460 | end 461 | end 462 | 463 | def rollback 464 | context.order.destroy 465 | end 466 | end 467 | ``` 468 | 469 | **NOTE:** The interactor that fails is *not* rolled back. Because every 470 | interactor should have a single purpose, there should be no need to clean up 471 | after any failed interactor. 472 | 473 | ## Testing Interactors 474 | 475 | When written correctly, an interactor is easy to test because it only *does* one 476 | thing. Take the following interactor: 477 | 478 | ```ruby 479 | class AuthenticateUser 480 | include Interactor 481 | 482 | def call 483 | if user = User.authenticate(context.email, context.password) 484 | context.user = user 485 | context.token = user.secret_token 486 | else 487 | context.fail!(message: "authenticate_user.failure") 488 | end 489 | end 490 | end 491 | ``` 492 | 493 | You can test just this interactor's single purpose and how it affects the 494 | context. 495 | 496 | ```ruby 497 | describe AuthenticateUser do 498 | subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") } 499 | 500 | describe ".call" do 501 | context "when given valid credentials" do 502 | let(:user) { double(:user, secret_token: "token") } 503 | 504 | before do 505 | allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user) 506 | end 507 | 508 | it "succeeds" do 509 | expect(context).to be_a_success 510 | end 511 | 512 | it "provides the user" do 513 | expect(context.user).to eq(user) 514 | end 515 | 516 | it "provides the user's secret token" do 517 | expect(context.token).to eq("token") 518 | end 519 | end 520 | 521 | context "when given invalid credentials" do 522 | before do 523 | allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil) 524 | end 525 | 526 | it "fails" do 527 | expect(context).to be_a_failure 528 | end 529 | 530 | it "provides a failure message" do 531 | expect(context.message).to be_present 532 | end 533 | end 534 | end 535 | end 536 | ``` 537 | 538 | [We](http://collectiveidea.com) use RSpec but the same approach applies to any 539 | testing framework. 540 | 541 | ### Isolation 542 | 543 | You may notice that we stub `User.authenticate` in our test rather than creating 544 | users in the database. That's because our purpose in 545 | `spec/interactors/authenticate_user_spec.rb` is to test just the 546 | `AuthenticateUser` interactor. The `User.authenticate` method is put through its 547 | own paces in `spec/models/user_spec.rb`. 548 | 549 | It's a good idea to define your own interfaces to your models. Doing so makes it 550 | easy to draw a line between which responsibilities belong to the interactor and 551 | which to the model. The `User.authenticate` method is a good, clear line. 552 | Imagine the interactor otherwise: 553 | 554 | ```ruby 555 | class AuthenticateUser 556 | include Interactor 557 | 558 | def call 559 | user = User.where(email: context.email).first 560 | 561 | # Yuck! 562 | if user && BCrypt::Password.new(user.password_digest) == context.password 563 | context.user = user 564 | else 565 | context.fail!(message: "authenticate_user.failure") 566 | end 567 | end 568 | end 569 | ``` 570 | 571 | It would be very difficult to test this interactor in isolation and even if you 572 | did, as soon as you change your ORM or your encryption algorithm (both model 573 | concerns), your interactors (business concerns) break. 574 | 575 | *Draw clear lines.* 576 | 577 | ### Integration 578 | 579 | While it's important to test your interactors in isolation, it's just as 580 | important to write good integration or acceptance tests. 581 | 582 | One of the pitfalls of testing in isolation is that when you stub a method, you 583 | could be hiding the fact that the method is broken, has changed or doesn't even 584 | exist. 585 | 586 | When you write full-stack tests that tie all of the pieces together, you can be 587 | sure that your application's individual pieces are working together as expected. 588 | That becomes even more important when you add a new layer to your code like 589 | interactors. 590 | 591 | **TIP:** If you track your test coverage, try for 100% coverage *before* 592 | integrations tests. Then keep writing integration tests until you sleep well at 593 | night. 594 | 595 | ### Controllers 596 | 597 | One of the advantages of using interactors is how much they simplify controllers 598 | and their tests. Because you're testing your interactors thoroughly in isolation 599 | as well as in integration tests (right?), you can remove your business logic 600 | from your controller tests. 601 | 602 | ```ruby 603 | class SessionsController < ApplicationController 604 | def create 605 | result = AuthenticateUser.call(session_params) 606 | 607 | if result.success? 608 | session[:user_token] = result.token 609 | redirect_to result.user 610 | else 611 | flash.now[:message] = t(result.message) 612 | render :new 613 | end 614 | end 615 | 616 | private 617 | 618 | def session_params 619 | params.require(:session).permit(:email, :password) 620 | end 621 | end 622 | ``` 623 | 624 | ```ruby 625 | describe SessionsController do 626 | describe "#create" do 627 | before do 628 | expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context) 629 | end 630 | 631 | context "when successful" do 632 | let(:user) { double(:user, id: 1) } 633 | let(:context) { double(:context, success?: true, user: user, token: "token") } 634 | 635 | it "saves the user's secret token in the session" do 636 | expect { 637 | post :create, session: { email: "john@doe.com", password: "secret" } 638 | }.to change { 639 | session[:user_token] 640 | }.from(nil).to("token") 641 | end 642 | 643 | it "redirects to the homepage" do 644 | response = post :create, session: { email: "john@doe.com", password: "secret" } 645 | 646 | expect(response).to redirect_to(user_path(user)) 647 | end 648 | end 649 | 650 | context "when unsuccessful" do 651 | let(:context) { double(:context, success?: false, message: "message") } 652 | 653 | it "sets a flash message" do 654 | expect { 655 | post :create, session: { email: "john@doe.com", password: "secret" } 656 | }.to change { 657 | flash[:message] 658 | }.from(nil).to(I18n.translate("message")) 659 | end 660 | 661 | it "renders the login form" do 662 | response = post :create, session: { email: "john@doe.com", password: "secret" } 663 | 664 | expect(response).to render_template(:new) 665 | end 666 | end 667 | end 668 | end 669 | ``` 670 | 671 | This controller test will have to change very little during the life of the 672 | application because all of the magic happens in the interactor. 673 | 674 | ### Rails 675 | 676 | [We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We 677 | put our interactors in `app/interactors` and we name them as verbs: 678 | 679 | * `AddProductToCart` 680 | * `AuthenticateUser` 681 | * `PlaceOrder` 682 | * `RegisterUser` 683 | * `RemoveProductFromCart` 684 | 685 | See: [Interactor Rails](https://github.com/collectiveidea/interactor-rails) 686 | 687 | ## Contributions 688 | 689 | Interactor is open source and contributions from the community are encouraged! 690 | No contribution is too small. 691 | 692 | See Interactor's 693 | [contribution guidelines](CONTRIBUTING.md) for more information. 694 | 695 | ## Thank You 696 | 697 | A very special thank you to [Attila Domokos](https://github.com/adomokos) for 698 | his fantastic work on [LightService](https://github.com/adomokos/light-service). 699 | Interactor is inspired heavily by the concepts put to code by Attila. 700 | 701 | Interactor was born from a desire for a slightly simplified interface. We 702 | understand that this is a matter of personal preference, so please take a look 703 | at LightService as well! 704 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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] 8 | -------------------------------------------------------------------------------- /interactor.gemspec: -------------------------------------------------------------------------------- 1 | require "English" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "interactor" 5 | spec.version = "3.1.2" 6 | 7 | spec.author = "Collective Idea" 8 | spec.email = "info@collectiveidea.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/collectiveidea/interactor" 12 | spec.license = "MIT" 13 | 14 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 15 | 16 | spec.add_dependency "ostruct" 17 | spec.add_development_dependency "bundler" 18 | spec.add_development_dependency "rake" 19 | end 20 | -------------------------------------------------------------------------------- /lib/interactor.rb: -------------------------------------------------------------------------------- 1 | require "interactor/context" 2 | require "interactor/error" 3 | require "interactor/hooks" 4 | require "interactor/organizer" 5 | 6 | # Public: Interactor methods. Because Interactor is a module, custom Interactor 7 | # classes should include Interactor rather than inherit from it. 8 | # 9 | # Examples 10 | # 11 | # class MyInteractor 12 | # include Interactor 13 | # 14 | # def call 15 | # puts context.foo 16 | # end 17 | # end 18 | module Interactor 19 | # Internal: Install Interactor's behavior in the given class. 20 | def self.included(base) 21 | base.class_eval do 22 | extend ClassMethods 23 | include Hooks 24 | 25 | # Public: Gets the Interactor::Context of the Interactor instance. 26 | attr_reader :context 27 | end 28 | end 29 | 30 | # Internal: Interactor class methods. 31 | module ClassMethods 32 | # Public: Invoke an Interactor. This is the primary public API method to an 33 | # interactor. 34 | # 35 | # context - A Hash whose key/value pairs are used in initializing a new 36 | # Interactor::Context object. An existing Interactor::Context may 37 | # also be given. (default: {}) 38 | # 39 | # Examples 40 | # 41 | # MyInteractor.call(foo: "bar") 42 | # # => # 43 | # 44 | # MyInteractor.call 45 | # # => # 46 | # 47 | # Returns the resulting Interactor::Context after manipulation by the 48 | # interactor. 49 | def call(context = {}) 50 | new(context).tap(&:run).context 51 | end 52 | 53 | # Public: Invoke an Interactor. The "call!" method behaves identically to 54 | # the "call" method with one notable exception. If the context is failed 55 | # during invocation of the interactor, the Interactor::Failure is raised. 56 | # 57 | # context - A Hash whose key/value pairs are used in initializing a new 58 | # Interactor::Context object. An existing Interactor::Context may 59 | # also be given. (default: {}) 60 | # 61 | # Examples 62 | # 63 | # MyInteractor.call!(foo: "bar") 64 | # # => # 65 | # 66 | # MyInteractor.call! 67 | # # => # 68 | # 69 | # MyInteractor.call!(foo: "baz") 70 | # # => Interactor::Failure: # 71 | # 72 | # Returns the resulting Interactor::Context after manipulation by the 73 | # interactor. 74 | # Raises Interactor::Failure if the context is failed. 75 | def call!(context = {}) 76 | new(context).tap(&:run!).context 77 | end 78 | end 79 | 80 | # Internal: Initialize an Interactor. 81 | # 82 | # context - A Hash whose key/value pairs are used in initializing the 83 | # interactor's context. An existing Interactor::Context may also be 84 | # given. (default: {}) 85 | # 86 | # Examples 87 | # 88 | # MyInteractor.new(foo: "bar") 89 | # # => #> 90 | # 91 | # MyInteractor.new 92 | # # => #> 93 | def initialize(context = {}) 94 | @context = Context.build(context) 95 | end 96 | 97 | # Internal: Invoke an interactor instance along with all defined hooks. The 98 | # "run" method is used internally by the "call" class method. The following 99 | # are equivalent: 100 | # 101 | # MyInteractor.call(foo: "bar") 102 | # # => # 103 | # 104 | # interactor = MyInteractor.new(foo: "bar") 105 | # interactor.run 106 | # interactor.context 107 | # # => # 108 | # 109 | # After successful invocation of the interactor, the instance is tracked 110 | # within the context. If the context is failed or any error is raised, the 111 | # context is rolled back. 112 | # 113 | # Returns nothing. 114 | def run 115 | run! 116 | rescue Failure => e 117 | if context.object_id != e.context.object_id 118 | raise 119 | end 120 | end 121 | 122 | # Internal: Invoke an Interactor instance along with all defined hooks. The 123 | # "run!" method is used internally by the "call!" class method. The following 124 | # are equivalent: 125 | # 126 | # MyInteractor.call!(foo: "bar") 127 | # # => # 128 | # 129 | # interactor = MyInteractor.new(foo: "bar") 130 | # interactor.run! 131 | # interactor.context 132 | # # => # 133 | # 134 | # After successful invocation of the interactor, the instance is tracked 135 | # within the context. If the context is failed or any error is raised, the 136 | # context is rolled back. 137 | # 138 | # The "run!" method behaves identically to the "run" method with one notable 139 | # exception. If the context is failed during invocation of the interactor, 140 | # the Interactor::Failure is raised. 141 | # 142 | # Returns nothing. 143 | # Raises Interactor::Failure if the context is failed. 144 | def run! 145 | with_hooks do 146 | call 147 | context.called!(self) 148 | end 149 | rescue 150 | context.rollback! 151 | raise 152 | end 153 | 154 | # Public: Invoke an Interactor instance without any hooks, tracking, or 155 | # rollback. It is expected that the "call" instance method is overwritten for 156 | # each interactor class. 157 | # 158 | # Returns nothing. 159 | def call 160 | end 161 | 162 | # Public: Reverse prior invocation of an Interactor instance. Any interactor 163 | # class that requires undoing upon downstream failure is expected to overwrite 164 | # the "rollback" instance method. 165 | # 166 | # Returns nothing. 167 | def rollback 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/interactor/context.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | 3 | module Interactor 4 | # Public: The object for tracking state of an Interactor's invocation. The 5 | # context is used to initialize the interactor with the information required 6 | # for invocation. The interactor manipulates the context to produce the result 7 | # of invocation. 8 | # 9 | # The context is the mechanism by which success and failure are determined and 10 | # the context is responsible for tracking individual interactor invocations 11 | # for the purpose of rollback. 12 | # 13 | # The context may be manipulated using arbitrary getter and setter methods. 14 | # 15 | # Examples 16 | # 17 | # context = Interactor::Context.new 18 | # # => # 19 | # context.foo = "bar" 20 | # # => "bar" 21 | # context 22 | # # => # 23 | # context.hello = "world" 24 | # # => "world" 25 | # context 26 | # # => # 27 | # context.foo = "baz" 28 | # # => "baz" 29 | # context 30 | # # => # 31 | class Context < OpenStruct 32 | # Internal: Initialize an Interactor::Context or preserve an existing one. 33 | # If the argument given is an Interactor::Context, the argument is returned. 34 | # Otherwise, a new Interactor::Context is initialized from the provided 35 | # hash. 36 | # 37 | # The "build" method is used during interactor initialization. 38 | # 39 | # context - A Hash whose key/value pairs are used in initializing a new 40 | # Interactor::Context object. If an existing Interactor::Context 41 | # is given, it is simply returned. (default: {}) 42 | # 43 | # Examples 44 | # 45 | # context = Interactor::Context.build(foo: "bar") 46 | # # => # 47 | # context.object_id 48 | # # => 2170969340 49 | # context = Interactor::Context.build(context) 50 | # # => # 51 | # context.object_id 52 | # # => 2170969340 53 | # 54 | # Returns the Interactor::Context. 55 | def self.build(context = {}) 56 | if self === context 57 | context 58 | else 59 | new(context) 60 | end 61 | end 62 | 63 | # Public: Whether the Interactor::Context is successful. By default, a new 64 | # context is successful and only changes when explicitly failed. 65 | # 66 | # The "success?" method is the inverse of the "failure?" method. 67 | # 68 | # Examples 69 | # 70 | # context = Interactor::Context.new 71 | # # => # 72 | # context.success? 73 | # # => true 74 | # context.fail! 75 | # # => Interactor::Failure: # 76 | # context.success? 77 | # # => false 78 | # 79 | # Returns true by default or false if failed. 80 | def success? 81 | !failure? 82 | end 83 | 84 | # Public: Whether the Interactor::Context has failed. By default, a new 85 | # context is successful and only changes when explicitly failed. 86 | # 87 | # The "failure?" method is the inverse of the "success?" method. 88 | # 89 | # Examples 90 | # 91 | # context = Interactor::Context.new 92 | # # => # 93 | # context.failure? 94 | # # => false 95 | # context.fail! 96 | # # => Interactor::Failure: # 97 | # context.failure? 98 | # # => true 99 | # 100 | # Returns false by default or true if failed. 101 | def failure? 102 | @failure || false 103 | end 104 | 105 | # Public: Fail the Interactor::Context. Failing a context raises an error 106 | # that may be rescued by the calling interactor. The context is also flagged 107 | # as having failed. 108 | # 109 | # Optionally the caller may provide a hash of key/value pairs to be merged 110 | # into the context before failure. 111 | # 112 | # context - A Hash whose key/value pairs are merged into the existing 113 | # Interactor::Context instance. (default: {}) 114 | # 115 | # Examples 116 | # 117 | # context = Interactor::Context.new 118 | # # => # 119 | # context.fail! 120 | # # => Interactor::Failure: # 121 | # context.fail! rescue false 122 | # # => false 123 | # context.fail!(foo: "baz") 124 | # # => Interactor::Failure: # 125 | # 126 | # Raises Interactor::Failure initialized with the Interactor::Context. 127 | def fail!(context = {}) 128 | context.each { |key, value| self[key.to_sym] = value } 129 | @failure = true 130 | raise Failure, self 131 | end 132 | 133 | # Internal: Track that an Interactor has been called. The "called!" method 134 | # is used by the interactor being invoked with this context. After an 135 | # interactor is successfully called, the interactor instance is tracked in 136 | # the context for the purpose of potential future rollback. 137 | # 138 | # interactor - An Interactor instance that has been successfully called. 139 | # 140 | # Returns nothing. 141 | def called!(interactor) 142 | _called << interactor 143 | end 144 | 145 | # Public: Roll back the Interactor::Context. Any interactors to which this 146 | # context has been passed and which have been successfully called are asked 147 | # to roll themselves back by invoking their "rollback" instance methods. 148 | # 149 | # Examples 150 | # 151 | # context = MyInteractor.call(foo: "bar") 152 | # # => # 153 | # context.rollback! 154 | # # => true 155 | # context 156 | # # => # 157 | # 158 | # Returns true if rolled back successfully or false if already rolled back. 159 | def rollback! 160 | return false if @rolled_back 161 | _called.reverse_each(&:rollback) 162 | @rolled_back = true 163 | end 164 | 165 | # Internal: An Array of successfully called Interactor instances invoked 166 | # against this Interactor::Context instance. 167 | # 168 | # Examples 169 | # 170 | # context = Interactor::Context.new 171 | # # => # 172 | # context._called 173 | # # => [] 174 | # 175 | # context = MyInteractor.call(foo: "bar") 176 | # # => # 177 | # context._called 178 | # # => [#>] 179 | # 180 | # Returns an Array of Interactor instances or an empty Array. 181 | def _called 182 | @called ||= [] 183 | end 184 | 185 | # Internal: Support for ruby 3.0 pattern matching 186 | # 187 | # Examples 188 | # 189 | # context = MyInteractor.call(foo: "bar") 190 | # 191 | # # => # 192 | # context => { foo: } 193 | # foo == "bar" 194 | # # => true 195 | # 196 | # 197 | # case context 198 | # in success: true, result: { first:, second: } 199 | # do_stuff(first, second) 200 | # in failure: true, error_message: 201 | # log_error(message: error_message) 202 | # end 203 | # 204 | # Returns the context as a hash, including success and failure 205 | def deconstruct_keys(keys) 206 | to_h.merge( 207 | success: success?, 208 | failure: failure? 209 | ) 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/interactor/error.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | # Internal: Error raised during Interactor::Context failure. The error stores 3 | # a copy of the failed context for debugging purposes. 4 | class Failure < StandardError 5 | # Internal: Gets the Interactor::Context of the Interactor::Failure 6 | # instance. 7 | attr_reader :context 8 | 9 | # Internal: Initialize an Interactor::Failure. 10 | # 11 | # context - An Interactor::Context to be stored within the 12 | # Interactor::Failure instance. (default: nil) 13 | # 14 | # Examples 15 | # 16 | # Interactor::Failure.new 17 | # # => # 18 | # 19 | # context = Interactor::Context.new(foo: "bar") 20 | # # => # 21 | # Interactor::Failure.new(context) 22 | # # => #> 23 | # 24 | # raise Interactor::Failure, context 25 | # # => Interactor::Failure: # 26 | def initialize(context = nil) 27 | @context = context 28 | super 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/interactor/hooks.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | # Internal: Methods relating to supporting hooks around Interactor invocation. 3 | module Hooks 4 | # Internal: Install Interactor's behavior in the given class. 5 | def self.included(base) 6 | base.class_eval do 7 | extend ClassMethods 8 | end 9 | end 10 | 11 | # Internal: Interactor::Hooks class methods. 12 | module ClassMethods 13 | # Public: Declare hooks to run around Interactor invocation. The around 14 | # method may be called multiple times; subsequent calls append declared 15 | # hooks to existing around hooks. 16 | # 17 | # hooks - Zero or more Symbol method names representing instance methods 18 | # to be called around interactor invocation. Each instance method 19 | # invocation receives an argument representing the next link in 20 | # the around hook chain. 21 | # block - An optional block to be executed as a hook. If given, the block 22 | # is executed after methods corresponding to any given Symbols. 23 | # 24 | # Examples 25 | # 26 | # class MyInteractor 27 | # include Interactor 28 | # 29 | # around :time_execution 30 | # 31 | # around do |interactor| 32 | # puts "started" 33 | # interactor.call 34 | # puts "finished" 35 | # end 36 | # 37 | # def call 38 | # puts "called" 39 | # end 40 | # 41 | # private 42 | # 43 | # def time_execution(interactor) 44 | # context.start_time = Time.now 45 | # interactor.call 46 | # context.finish_time = Time.now 47 | # end 48 | # end 49 | # 50 | # Returns nothing. 51 | def around(*hooks, &block) 52 | hooks << block if block 53 | hooks.each { |hook| around_hooks.push(hook) } 54 | end 55 | 56 | # Public: Declare hooks to run before Interactor invocation. The before 57 | # method may be called multiple times; subsequent calls append declared 58 | # hooks to existing before hooks. 59 | # 60 | # hooks - Zero or more Symbol method names representing instance methods 61 | # to be called before interactor invocation. 62 | # block - An optional block to be executed as a hook. If given, the block 63 | # is executed after methods corresponding to any given Symbols. 64 | # 65 | # Examples 66 | # 67 | # class MyInteractor 68 | # include Interactor 69 | # 70 | # before :set_start_time 71 | # 72 | # before do 73 | # puts "started" 74 | # end 75 | # 76 | # def call 77 | # puts "called" 78 | # end 79 | # 80 | # private 81 | # 82 | # def set_start_time 83 | # context.start_time = Time.now 84 | # end 85 | # end 86 | # 87 | # Returns nothing. 88 | def before(*hooks, &block) 89 | hooks << block if block 90 | hooks.each { |hook| before_hooks.push(hook) } 91 | end 92 | 93 | # Public: Declare hooks to run after Interactor invocation. The after 94 | # method may be called multiple times; subsequent calls prepend declared 95 | # hooks to existing after hooks. 96 | # 97 | # hooks - Zero or more Symbol method names representing instance methods 98 | # to be called after interactor invocation. 99 | # block - An optional block to be executed as a hook. If given, the block 100 | # is executed before methods corresponding to any given Symbols. 101 | # 102 | # Examples 103 | # 104 | # class MyInteractor 105 | # include Interactor 106 | # 107 | # after :set_finish_time 108 | # 109 | # after do 110 | # puts "finished" 111 | # end 112 | # 113 | # def call 114 | # puts "called" 115 | # end 116 | # 117 | # private 118 | # 119 | # def set_finish_time 120 | # context.finish_time = Time.now 121 | # end 122 | # end 123 | # 124 | # Returns nothing. 125 | def after(*hooks, &block) 126 | hooks << block if block 127 | hooks.each { |hook| after_hooks.unshift(hook) } 128 | end 129 | 130 | # Internal: An Array of declared hooks to run around Interactor 131 | # invocation. The hooks appear in the order in which they will be run. 132 | # 133 | # Examples 134 | # 135 | # class MyInteractor 136 | # include Interactor 137 | # 138 | # around :time_execution, :use_transaction 139 | # end 140 | # 141 | # MyInteractor.around_hooks 142 | # # => [:time_execution, :use_transaction] 143 | # 144 | # Returns an Array of Symbols and Procs. 145 | def around_hooks 146 | @around_hooks ||= [] 147 | end 148 | 149 | # Internal: An Array of declared hooks to run before Interactor 150 | # invocation. The hooks appear in the order in which they will be run. 151 | # 152 | # Examples 153 | # 154 | # class MyInteractor 155 | # include Interactor 156 | # 157 | # before :set_start_time, :say_hello 158 | # end 159 | # 160 | # MyInteractor.before_hooks 161 | # # => [:set_start_time, :say_hello] 162 | # 163 | # Returns an Array of Symbols and Procs. 164 | def before_hooks 165 | @before_hooks ||= [] 166 | end 167 | 168 | # Internal: An Array of declared hooks to run before Interactor 169 | # invocation. The hooks appear in the order in which they will be run. 170 | # 171 | # Examples 172 | # 173 | # class MyInteractor 174 | # include Interactor 175 | # 176 | # after :set_finish_time, :say_goodbye 177 | # end 178 | # 179 | # MyInteractor.after_hooks 180 | # # => [:say_goodbye, :set_finish_time] 181 | # 182 | # Returns an Array of Symbols and Procs. 183 | def after_hooks 184 | @after_hooks ||= [] 185 | end 186 | end 187 | 188 | private 189 | 190 | # Internal: Run around, before and after hooks around yielded execution. The 191 | # required block is surrounded with hooks and executed. 192 | # 193 | # Examples 194 | # 195 | # class MyProcessor 196 | # include Interactor::Hooks 197 | # 198 | # def process_with_hooks 199 | # with_hooks do 200 | # process 201 | # end 202 | # end 203 | # 204 | # def process 205 | # puts "processed!" 206 | # end 207 | # end 208 | # 209 | # Returns nothing. 210 | def with_hooks 211 | run_around_hooks do 212 | run_before_hooks 213 | yield 214 | run_after_hooks 215 | end 216 | end 217 | 218 | # Internal: Run around hooks. 219 | # 220 | # Returns nothing. 221 | def run_around_hooks(&block) 222 | self.class.around_hooks.reverse.inject(block) { |chain, hook| 223 | proc { run_hook(hook, chain) } 224 | }.call 225 | end 226 | 227 | # Internal: Run before hooks. 228 | # 229 | # Returns nothing. 230 | def run_before_hooks 231 | run_hooks(self.class.before_hooks) 232 | end 233 | 234 | # Internal: Run after hooks. 235 | # 236 | # Returns nothing. 237 | def run_after_hooks 238 | run_hooks(self.class.after_hooks) 239 | end 240 | 241 | # Internal: Run a colection of hooks. The "run_hooks" method is the common 242 | # interface by which collections of either before or after hooks are run. 243 | # 244 | # hooks - An Array of Symbol and Proc hooks. 245 | # 246 | # Returns nothing. 247 | def run_hooks(hooks) 248 | hooks.each { |hook| run_hook(hook) } 249 | end 250 | 251 | # Internal: Run an individual hook. The "run_hook" method is the common 252 | # interface by which an individual hook is run. If the given hook is a 253 | # symbol, the method is invoked whether public or private. If the hook is a 254 | # proc, the proc is evaluated in the context of the current instance. 255 | # 256 | # hook - A Symbol or Proc hook. 257 | # args - Zero or more arguments to be passed as block arguments into the 258 | # given block or as arguments into the method described by the given 259 | # Symbol method name. 260 | # 261 | # Returns nothing. 262 | def run_hook(hook, *args) 263 | hook.is_a?(Symbol) ? send(hook, *args) : instance_exec(*args, &hook) 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /lib/interactor/organizer.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | # Public: Interactor::Organizer methods. Because Interactor::Organizer is a 3 | # module, custom Interactor::Organizer classes should include 4 | # Interactor::Organizer rather than inherit from it. 5 | # 6 | # Examples 7 | # 8 | # class MyOrganizer 9 | # include Interactor::Organizer 10 | # 11 | # organize InteractorOne, InteractorTwo 12 | # end 13 | module Organizer 14 | # Internal: Install Interactor::Organizer's behavior in the given class. 15 | def self.included(base) 16 | base.class_eval do 17 | include Interactor 18 | 19 | extend ClassMethods 20 | include InstanceMethods 21 | end 22 | end 23 | 24 | # Internal: Interactor::Organizer class methods. 25 | module ClassMethods 26 | # Public: Declare Interactors to be invoked as part of the 27 | # Interactor::Organizer's invocation. These interactors are invoked in 28 | # the order in which they are declared. 29 | # 30 | # interactors - Zero or more (or an Array of) Interactor classes. 31 | # 32 | # Examples 33 | # 34 | # class MyFirstOrganizer 35 | # include Interactor::Organizer 36 | # 37 | # organize InteractorOne, InteractorTwo 38 | # end 39 | # 40 | # class MySecondOrganizer 41 | # include Interactor::Organizer 42 | # 43 | # organize [InteractorThree, InteractorFour] 44 | # end 45 | # 46 | # Returns nothing. 47 | def organize(*interactors) 48 | @organized = interactors.flatten 49 | end 50 | 51 | # Internal: An Array of declared Interactors to be invoked. 52 | # 53 | # Examples 54 | # 55 | # class MyOrganizer 56 | # include Interactor::Organizer 57 | # 58 | # organize InteractorOne, InteractorTwo 59 | # end 60 | # 61 | # MyOrganizer.organized 62 | # # => [InteractorOne, InteractorTwo] 63 | # 64 | # Returns an Array of Interactor classes or an empty Array. 65 | def organized 66 | @organized ||= [] 67 | end 68 | end 69 | 70 | # Internal: Interactor::Organizer instance methods. 71 | module InstanceMethods 72 | # Internal: Invoke the organized Interactors. An Interactor::Organizer is 73 | # expected not to define its own "#call" method in favor of this default 74 | # implementation. 75 | # 76 | # Returns nothing. 77 | def call 78 | self.class.organized.each do |interactor| 79 | interactor.call!(context) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Integration" do 2 | def build_interactor(&block) 3 | interactor = Class.new.send(:include, Interactor) 4 | interactor.class_eval(&block) if block 5 | interactor 6 | end 7 | 8 | def build_organizer(options = {}, &block) 9 | organizer = Class.new.send(:include, Interactor::Organizer) 10 | organizer.organize(options[:organize]) if options[:organize] 11 | organizer.class_eval(&block) if block 12 | organizer 13 | end 14 | 15 | # organizer 16 | # ├─ organizer2 17 | # │ ├─ interactor2a 18 | # │ ├─ interactor2b 19 | # │ └─ interactor2c 20 | # ├─ interactor3 21 | # ├─ organizer4 22 | # │ ├─ interactor4a 23 | # │ ├─ interactor4b 24 | # │ └─ interactor4c 25 | # └─ interactor5 26 | 27 | let(:organizer) { 28 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 29 | around do |interactor| 30 | context.steps << :around_before 31 | interactor.call 32 | context.steps << :around_after 33 | end 34 | 35 | before do 36 | context.steps << :before 37 | end 38 | 39 | after do 40 | context.steps << :after 41 | end 42 | end 43 | } 44 | 45 | let(:organizer2) { 46 | build_organizer(organize: [interactor2a, interactor2b, interactor2c]) do 47 | around do |interactor| 48 | context.steps << :around_before2 49 | interactor.call 50 | context.steps << :around_after2 51 | end 52 | 53 | before do 54 | context.steps << :before2 55 | end 56 | 57 | after do 58 | context.steps << :after2 59 | end 60 | end 61 | } 62 | 63 | let(:interactor2a) { 64 | build_interactor do 65 | around do |interactor| 66 | context.steps << :around_before2a 67 | interactor.call 68 | context.steps << :around_after2a 69 | end 70 | 71 | before do 72 | context.steps << :before2a 73 | end 74 | 75 | after do 76 | context.steps << :after2a 77 | end 78 | 79 | def call 80 | context.steps << :call2a 81 | end 82 | 83 | def rollback 84 | context.steps << :rollback2a 85 | end 86 | end 87 | } 88 | 89 | let(:interactor2b) { 90 | build_interactor do 91 | around do |interactor| 92 | context.steps << :around_before2b 93 | interactor.call 94 | context.steps << :around_after2b 95 | end 96 | 97 | before do 98 | context.steps << :before2b 99 | end 100 | 101 | after do 102 | context.steps << :after2b 103 | end 104 | 105 | def call 106 | context.steps << :call2b 107 | end 108 | 109 | def rollback 110 | context.steps << :rollback2b 111 | end 112 | end 113 | } 114 | 115 | let(:interactor2c) { 116 | build_interactor do 117 | around do |interactor| 118 | context.steps << :around_before2c 119 | interactor.call 120 | context.steps << :around_after2c 121 | end 122 | 123 | before do 124 | context.steps << :before2c 125 | end 126 | 127 | after do 128 | context.steps << :after2c 129 | end 130 | 131 | def call 132 | context.steps << :call2c 133 | end 134 | 135 | def rollback 136 | context.steps << :rollback2c 137 | end 138 | end 139 | } 140 | 141 | let(:interactor3) { 142 | build_interactor do 143 | around do |interactor| 144 | context.steps << :around_before3 145 | interactor.call 146 | context.steps << :around_after3 147 | end 148 | 149 | before do 150 | context.steps << :before3 151 | end 152 | 153 | after do 154 | context.steps << :after3 155 | end 156 | 157 | def call 158 | context.steps << :call3 159 | end 160 | 161 | def rollback 162 | context.steps << :rollback3 163 | end 164 | end 165 | } 166 | 167 | let(:organizer4) { 168 | build_organizer(organize: [interactor4a, interactor4b, interactor4c]) do 169 | around do |interactor| 170 | context.steps << :around_before4 171 | interactor.call 172 | context.steps << :around_after4 173 | end 174 | 175 | before do 176 | context.steps << :before4 177 | end 178 | 179 | after do 180 | context.steps << :after4 181 | end 182 | end 183 | } 184 | 185 | let(:interactor4a) { 186 | build_interactor do 187 | around do |interactor| 188 | context.steps << :around_before4a 189 | interactor.call 190 | context.steps << :around_after4a 191 | end 192 | 193 | before do 194 | context.steps << :before4a 195 | end 196 | 197 | after do 198 | context.steps << :after4a 199 | end 200 | 201 | def call 202 | context.steps << :call4a 203 | end 204 | 205 | def rollback 206 | context.steps << :rollback4a 207 | end 208 | end 209 | } 210 | 211 | let(:interactor4b) { 212 | build_interactor do 213 | around do |interactor| 214 | context.steps << :around_before4b 215 | interactor.call 216 | context.steps << :around_after4b 217 | end 218 | 219 | before do 220 | context.steps << :before4b 221 | end 222 | 223 | after do 224 | context.steps << :after4b 225 | end 226 | 227 | def call 228 | context.steps << :call4b 229 | end 230 | 231 | def rollback 232 | context.steps << :rollback4b 233 | end 234 | end 235 | } 236 | 237 | let(:interactor4c) { 238 | build_interactor do 239 | around do |interactor| 240 | context.steps << :around_before4c 241 | interactor.call 242 | context.steps << :around_after4c 243 | end 244 | 245 | before do 246 | context.steps << :before4c 247 | end 248 | 249 | after do 250 | context.steps << :after4c 251 | end 252 | 253 | def call 254 | context.steps << :call4c 255 | end 256 | 257 | def rollback 258 | context.steps << :rollback4c 259 | end 260 | end 261 | } 262 | 263 | let(:interactor5) { 264 | build_interactor do 265 | around do |interactor| 266 | context.steps << :around_before5 267 | interactor.call 268 | context.steps << :around_after5 269 | end 270 | 271 | before do 272 | context.steps << :before5 273 | end 274 | 275 | after do 276 | context.steps << :after5 277 | end 278 | 279 | def call 280 | context.steps << :call5 281 | end 282 | 283 | def rollback 284 | context.steps << :rollback5 285 | end 286 | end 287 | } 288 | 289 | let(:context) { Interactor::Context.new(steps: []) } 290 | 291 | context "when successful" do 292 | it "calls and runs hooks in the proper sequence" do 293 | expect { 294 | organizer.call(context) 295 | }.to change { 296 | context.steps 297 | }.from([]).to([ 298 | :around_before, :before, 299 | :around_before2, :before2, 300 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 301 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 302 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 303 | :after2, :around_after2, 304 | :around_before3, :before3, :call3, :after3, :around_after3, 305 | :around_before4, :before4, 306 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 307 | :around_before4b, :before4b, :call4b, :after4b, :around_after4b, 308 | :around_before4c, :before4c, :call4c, :after4c, :around_after4c, 309 | :after4, :around_after4, 310 | :around_before5, :before5, :call5, :after5, :around_after5, 311 | :after, :around_after 312 | ]) 313 | end 314 | end 315 | 316 | context "when an around hook fails early" do 317 | let(:organizer) { 318 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 319 | around do |interactor| 320 | context.fail! 321 | context.steps << :around_before 322 | interactor.call 323 | context.steps << :around_after 324 | end 325 | 326 | before do 327 | context.fail! 328 | context.steps << :before 329 | end 330 | 331 | after do 332 | context.steps << :after 333 | end 334 | end 335 | } 336 | 337 | it "aborts" do 338 | expect { 339 | organizer.call(context) 340 | }.not_to change { 341 | context.steps 342 | } 343 | end 344 | end 345 | 346 | context "when an around hook errors early" do 347 | let(:organizer) { 348 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 349 | around do |interactor| 350 | raise "foo" 351 | end 352 | 353 | before do 354 | context.fail! 355 | context.steps << :before 356 | end 357 | 358 | after do 359 | context.steps << :after 360 | end 361 | end 362 | } 363 | 364 | it "aborts" do 365 | expect { 366 | begin 367 | organizer.call(context) 368 | rescue 369 | nil 370 | end 371 | }.not_to change { 372 | context.steps 373 | } 374 | end 375 | 376 | it "raises the error" do 377 | expect { 378 | organizer.call(context) 379 | }.to raise_error("foo") 380 | end 381 | end 382 | 383 | context "when a before hook fails" do 384 | let(:organizer) { 385 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 386 | around do |interactor| 387 | context.steps << :around_before 388 | interactor.call 389 | context.steps << :around_after 390 | end 391 | 392 | before do 393 | context.fail! 394 | context.steps << :before 395 | end 396 | 397 | after do 398 | context.steps << :after 399 | end 400 | end 401 | } 402 | 403 | it "aborts" do 404 | expect { 405 | organizer.call(context) 406 | }.to change { 407 | context.steps 408 | }.from([]).to([ 409 | :around_before 410 | ]) 411 | end 412 | end 413 | 414 | context "when a before hook errors" do 415 | let(:organizer) { 416 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 417 | around do |interactor| 418 | context.steps << :around_before 419 | interactor.call 420 | context.steps << :around_after 421 | end 422 | 423 | before do 424 | raise "foo" 425 | end 426 | 427 | after do 428 | context.steps << :after 429 | end 430 | end 431 | } 432 | 433 | it "aborts" do 434 | expect { 435 | begin 436 | organizer.call(context) 437 | rescue 438 | nil 439 | end 440 | }.to change { 441 | context.steps 442 | }.from([]).to([ 443 | :around_before 444 | ]) 445 | end 446 | 447 | it "raises the error" do 448 | expect { 449 | organizer.call(context) 450 | }.to raise_error("foo") 451 | end 452 | end 453 | 454 | context "when an after hook fails" do 455 | let(:organizer) { 456 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 457 | around do |interactor| 458 | context.steps << :around_before 459 | interactor.call 460 | context.steps << :around_after 461 | end 462 | 463 | before do 464 | context.steps << :before 465 | end 466 | 467 | after do 468 | context.fail! 469 | context.steps << :after 470 | end 471 | end 472 | } 473 | 474 | it "rolls back successfully called interactors and the failed interactor" do 475 | expect { 476 | organizer.call(context) 477 | }.to change { 478 | context.steps 479 | }.from([]).to([ 480 | :around_before, :before, 481 | :around_before2, :before2, 482 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 483 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 484 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 485 | :after2, :around_after2, 486 | :around_before3, :before3, :call3, :after3, :around_after3, 487 | :around_before4, :before4, 488 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 489 | :around_before4b, :before4b, :call4b, :after4b, :around_after4b, 490 | :around_before4c, :before4c, :call4c, :after4c, :around_after4c, 491 | :after4, :around_after4, 492 | :around_before5, :before5, :call5, :after5, :around_after5, 493 | :rollback5, 494 | :rollback4c, 495 | :rollback4b, 496 | :rollback4a, 497 | :rollback3, 498 | :rollback2c, 499 | :rollback2b, 500 | :rollback2a 501 | ]) 502 | end 503 | end 504 | 505 | context "when an after hook errors" do 506 | let(:organizer) { 507 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 508 | around do |interactor| 509 | context.steps << :around_before 510 | interactor.call 511 | context.steps << :around_after 512 | end 513 | 514 | before do 515 | context.steps << :before 516 | end 517 | 518 | after do 519 | raise "foo" 520 | end 521 | end 522 | } 523 | 524 | it "rolls back successfully called interactors and the failed interactor" do 525 | expect { 526 | begin 527 | organizer.call(context) 528 | rescue 529 | nil 530 | end 531 | }.to change { 532 | context.steps 533 | }.from([]).to([ 534 | :around_before, :before, 535 | :around_before2, :before2, 536 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 537 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 538 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 539 | :after2, :around_after2, 540 | :around_before3, :before3, :call3, :after3, :around_after3, 541 | :around_before4, :before4, 542 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 543 | :around_before4b, :before4b, :call4b, :after4b, :around_after4b, 544 | :around_before4c, :before4c, :call4c, :after4c, :around_after4c, 545 | :after4, :around_after4, 546 | :around_before5, :before5, :call5, :after5, :around_after5, 547 | :rollback5, 548 | :rollback4c, 549 | :rollback4b, 550 | :rollback4a, 551 | :rollback3, 552 | :rollback2c, 553 | :rollback2b, 554 | :rollback2a 555 | ]) 556 | end 557 | 558 | it "raises the error" do 559 | expect { 560 | organizer.call(context) 561 | }.to raise_error("foo") 562 | end 563 | end 564 | 565 | context "when an around hook fails late" do 566 | let(:organizer) { 567 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 568 | around do |interactor| 569 | context.steps << :around_before 570 | interactor.call 571 | context.fail! 572 | context.steps << :around_after 573 | end 574 | 575 | before do 576 | context.steps << :before 577 | end 578 | 579 | after do 580 | context.steps << :after 581 | end 582 | end 583 | } 584 | 585 | it "rolls back successfully called interactors and the failed interactor" do 586 | expect { 587 | organizer.call(context) 588 | }.to change { 589 | context.steps 590 | }.from([]).to([ 591 | :around_before, :before, 592 | :around_before2, :before2, 593 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 594 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 595 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 596 | :after2, :around_after2, 597 | :around_before3, :before3, :call3, :after3, :around_after3, 598 | :around_before4, :before4, 599 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 600 | :around_before4b, :before4b, :call4b, :after4b, :around_after4b, 601 | :around_before4c, :before4c, :call4c, :after4c, :around_after4c, 602 | :after4, :around_after4, 603 | :around_before5, :before5, :call5, :after5, :around_after5, 604 | :after, 605 | :rollback5, 606 | :rollback4c, 607 | :rollback4b, 608 | :rollback4a, 609 | :rollback3, 610 | :rollback2c, 611 | :rollback2b, 612 | :rollback2a 613 | ]) 614 | end 615 | end 616 | 617 | context "when an around hook errors late" do 618 | let(:organizer) { 619 | build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do 620 | around do |interactor| 621 | context.steps << :around_before 622 | interactor.call 623 | raise "foo" 624 | end 625 | 626 | before do 627 | context.steps << :before 628 | end 629 | 630 | after do 631 | context.steps << :after 632 | end 633 | end 634 | } 635 | 636 | it "rolls back successfully called interactors and the failed interactor" do 637 | expect { 638 | begin 639 | organizer.call(context) 640 | rescue 641 | nil 642 | end 643 | }.to change { 644 | context.steps 645 | }.from([]).to([ 646 | :around_before, :before, 647 | :around_before2, :before2, 648 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 649 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 650 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 651 | :after2, :around_after2, 652 | :around_before3, :before3, :call3, :after3, :around_after3, 653 | :around_before4, :before4, 654 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 655 | :around_before4b, :before4b, :call4b, :after4b, :around_after4b, 656 | :around_before4c, :before4c, :call4c, :after4c, :around_after4c, 657 | :after4, :around_after4, 658 | :around_before5, :before5, :call5, :after5, :around_after5, 659 | :after, 660 | :rollback5, 661 | :rollback4c, 662 | :rollback4b, 663 | :rollback4a, 664 | :rollback3, 665 | :rollback2c, 666 | :rollback2b, 667 | :rollback2a 668 | ]) 669 | end 670 | 671 | it "raises the error" do 672 | expect { 673 | organizer.call(context) 674 | }.to raise_error("foo") 675 | end 676 | end 677 | 678 | context "when a nested around hook fails early" do 679 | let(:interactor3) { 680 | build_interactor do 681 | around do |interactor| 682 | context.fail! 683 | context.steps << :around_before3 684 | interactor.call 685 | context.steps << :around_after3 686 | end 687 | 688 | before do 689 | context.steps << :before3 690 | end 691 | 692 | after do 693 | context.steps << :after3 694 | end 695 | 696 | def call 697 | context.steps << :call3 698 | end 699 | 700 | def rollback 701 | context.steps << :rollback3 702 | end 703 | end 704 | } 705 | 706 | it "rolls back successfully called interactors" do 707 | expect { 708 | organizer.call(context) 709 | }.to change { 710 | context.steps 711 | }.from([]).to([ 712 | :around_before, :before, 713 | :around_before2, :before2, 714 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 715 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 716 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 717 | :after2, :around_after2, 718 | :rollback2c, 719 | :rollback2b, 720 | :rollback2a 721 | ]) 722 | end 723 | end 724 | 725 | context "when a nested around hook errors early" do 726 | let(:interactor3) { 727 | build_interactor do 728 | around do |interactor| 729 | raise "foo" 730 | end 731 | 732 | before do 733 | context.steps << :before3 734 | end 735 | 736 | after do 737 | context.steps << :after3 738 | end 739 | 740 | def call 741 | context.steps << :call3 742 | end 743 | 744 | def rollback 745 | context.steps << :rollback3 746 | end 747 | end 748 | } 749 | 750 | it "rolls back successfully called interactors" do 751 | expect { 752 | begin 753 | organizer.call(context) 754 | rescue 755 | nil 756 | end 757 | }.to change { 758 | context.steps 759 | }.from([]).to([ 760 | :around_before, :before, 761 | :around_before2, :before2, 762 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 763 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 764 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 765 | :after2, :around_after2, 766 | :rollback2c, 767 | :rollback2b, 768 | :rollback2a 769 | ]) 770 | end 771 | 772 | it "raises the error" do 773 | expect { 774 | organizer.call(context) 775 | }.to raise_error("foo") 776 | end 777 | end 778 | 779 | context "when a nested before hook fails" do 780 | let(:interactor3) { 781 | build_interactor do 782 | around do |interactor| 783 | context.steps << :around_before3 784 | interactor.call 785 | context.steps << :around_after3 786 | end 787 | 788 | before do 789 | context.fail! 790 | context.steps << :before3 791 | end 792 | 793 | after do 794 | context.steps << :after3 795 | end 796 | 797 | def call 798 | context.steps << :call3 799 | end 800 | 801 | def rollback 802 | context.steps << :rollback3 803 | end 804 | end 805 | } 806 | 807 | it "rolls back successfully called interactors" do 808 | expect { 809 | organizer.call(context) 810 | }.to change { 811 | context.steps 812 | }.from([]).to([ 813 | :around_before, :before, 814 | :around_before2, :before2, 815 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 816 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 817 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 818 | :after2, :around_after2, 819 | :around_before3, 820 | :rollback2c, 821 | :rollback2b, 822 | :rollback2a 823 | ]) 824 | end 825 | end 826 | 827 | context "when a nested before hook errors" do 828 | let(:interactor3) { 829 | build_interactor do 830 | around do |interactor| 831 | context.steps << :around_before3 832 | interactor.call 833 | context.steps << :around_after3 834 | end 835 | 836 | before do 837 | raise "foo" 838 | end 839 | 840 | after do 841 | context.steps << :after3 842 | end 843 | 844 | def call 845 | context.steps << :call3 846 | end 847 | 848 | def rollback 849 | context.steps << :rollback3 850 | end 851 | end 852 | } 853 | 854 | it "rolls back successfully called interactors" do 855 | expect { 856 | begin 857 | organizer.call(context) 858 | rescue 859 | nil 860 | end 861 | }.to change { 862 | context.steps 863 | }.from([]).to([ 864 | :around_before, :before, 865 | :around_before2, :before2, 866 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 867 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 868 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 869 | :after2, :around_after2, 870 | :around_before3, 871 | :rollback2c, 872 | :rollback2b, 873 | :rollback2a 874 | ]) 875 | end 876 | 877 | it "raises the error" do 878 | expect { 879 | organizer.call(context) 880 | }.to raise_error("foo") 881 | end 882 | end 883 | 884 | context "when a nested call fails" do 885 | let(:interactor3) { 886 | build_interactor do 887 | around do |interactor| 888 | context.steps << :around_before3 889 | interactor.call 890 | context.steps << :around_after3 891 | end 892 | 893 | before do 894 | context.steps << :before3 895 | end 896 | 897 | after do 898 | context.steps << :after3 899 | end 900 | 901 | def call 902 | context.fail! 903 | context.steps << :call3 904 | end 905 | 906 | def rollback 907 | context.steps << :rollback3 908 | end 909 | end 910 | } 911 | 912 | it "rolls back successfully called interactors" do 913 | expect { 914 | organizer.call(context) 915 | }.to change { 916 | context.steps 917 | }.from([]).to([ 918 | :around_before, :before, 919 | :around_before2, :before2, 920 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 921 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 922 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 923 | :after2, :around_after2, 924 | :around_before3, :before3, 925 | :rollback2c, 926 | :rollback2b, 927 | :rollback2a 928 | ]) 929 | end 930 | end 931 | 932 | context "when a nested call errors" do 933 | let(:interactor3) { 934 | build_interactor do 935 | around do |interactor| 936 | context.steps << :around_before3 937 | interactor.call 938 | context.steps << :around_after3 939 | end 940 | 941 | before do 942 | context.steps << :before3 943 | end 944 | 945 | after do 946 | context.steps << :after3 947 | end 948 | 949 | def call 950 | raise "foo" 951 | end 952 | 953 | def rollback 954 | context.steps << :rollback3 955 | end 956 | end 957 | } 958 | 959 | it "rolls back successfully called interactors" do 960 | expect { 961 | begin 962 | organizer.call(context) 963 | rescue 964 | nil 965 | end 966 | }.to change { 967 | context.steps 968 | }.from([]).to([ 969 | :around_before, :before, 970 | :around_before2, :before2, 971 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 972 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 973 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 974 | :after2, :around_after2, 975 | :around_before3, :before3, 976 | :rollback2c, 977 | :rollback2b, 978 | :rollback2a 979 | ]) 980 | end 981 | 982 | it "raises the error" do 983 | expect { 984 | organizer.call(context) 985 | }.to raise_error("foo") 986 | end 987 | end 988 | 989 | context "when a nested after hook fails" do 990 | let(:interactor3) { 991 | build_interactor do 992 | around do |interactor| 993 | context.steps << :around_before3 994 | interactor.call 995 | context.steps << :around_after3 996 | end 997 | 998 | before do 999 | context.steps << :before3 1000 | end 1001 | 1002 | after do 1003 | context.fail! 1004 | context.steps << :after3 1005 | end 1006 | 1007 | def call 1008 | context.steps << :call3 1009 | end 1010 | 1011 | def rollback 1012 | context.steps << :rollback3 1013 | end 1014 | end 1015 | } 1016 | 1017 | it "rolls back successfully called interactors and the failed interactor" do 1018 | expect { 1019 | organizer.call(context) 1020 | }.to change { 1021 | context.steps 1022 | }.from([]).to([ 1023 | :around_before, :before, 1024 | :around_before2, :before2, 1025 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1026 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1027 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1028 | :after2, :around_after2, 1029 | :around_before3, :before3, :call3, 1030 | :rollback3, 1031 | :rollback2c, 1032 | :rollback2b, 1033 | :rollback2a 1034 | ]) 1035 | end 1036 | end 1037 | 1038 | context "when a nested after hook errors" do 1039 | let(:interactor3) { 1040 | build_interactor do 1041 | around do |interactor| 1042 | context.steps << :around_before3 1043 | interactor.call 1044 | context.steps << :around_after3 1045 | end 1046 | 1047 | before do 1048 | context.steps << :before3 1049 | end 1050 | 1051 | after do 1052 | raise "foo" 1053 | end 1054 | 1055 | def call 1056 | context.steps << :call3 1057 | end 1058 | 1059 | def rollback 1060 | context.steps << :rollback3 1061 | end 1062 | end 1063 | } 1064 | 1065 | it "rolls back successfully called interactors and the failed interactor" do 1066 | expect { 1067 | begin 1068 | organizer.call(context) 1069 | rescue 1070 | nil 1071 | end 1072 | }.to change { 1073 | context.steps 1074 | }.from([]).to([ 1075 | :around_before, :before, 1076 | :around_before2, :before2, 1077 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1078 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1079 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1080 | :after2, :around_after2, 1081 | :around_before3, :before3, :call3, 1082 | :rollback3, 1083 | :rollback2c, 1084 | :rollback2b, 1085 | :rollback2a 1086 | ]) 1087 | end 1088 | 1089 | it "raises the error" do 1090 | expect { 1091 | organizer.call(context) 1092 | }.to raise_error("foo") 1093 | end 1094 | end 1095 | 1096 | context "when a nested around hook fails late" do 1097 | let(:interactor3) { 1098 | build_interactor do 1099 | around do |interactor| 1100 | context.steps << :around_before3 1101 | interactor.call 1102 | context.fail! 1103 | context.steps << :around_after3 1104 | end 1105 | 1106 | before do 1107 | context.steps << :before3 1108 | end 1109 | 1110 | after do 1111 | context.steps << :after3 1112 | end 1113 | 1114 | def call 1115 | context.steps << :call3 1116 | end 1117 | 1118 | def rollback 1119 | context.steps << :rollback3 1120 | end 1121 | end 1122 | } 1123 | 1124 | it "rolls back successfully called interactors and the failed interactor" do 1125 | expect { 1126 | organizer.call(context) 1127 | }.to change { 1128 | context.steps 1129 | }.from([]).to([ 1130 | :around_before, :before, 1131 | :around_before2, :before2, 1132 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1133 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1134 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1135 | :after2, :around_after2, 1136 | :around_before3, :before3, :call3, :after3, 1137 | :rollback3, 1138 | :rollback2c, 1139 | :rollback2b, 1140 | :rollback2a 1141 | ]) 1142 | end 1143 | end 1144 | 1145 | context "when a nested around hook errors late" do 1146 | let(:interactor3) { 1147 | build_interactor do 1148 | around do |interactor| 1149 | context.steps << :around_before3 1150 | interactor.call 1151 | raise "foo" 1152 | end 1153 | 1154 | before do 1155 | context.steps << :before3 1156 | end 1157 | 1158 | after do 1159 | context.steps << :after3 1160 | end 1161 | 1162 | def call 1163 | context.steps << :call3 1164 | end 1165 | 1166 | def rollback 1167 | context.steps << :rollback3 1168 | end 1169 | end 1170 | } 1171 | 1172 | it "rolls back successfully called interactors and the failed interactor" do 1173 | expect { 1174 | begin 1175 | organizer.call(context) 1176 | rescue 1177 | nil 1178 | end 1179 | }.to change { 1180 | context.steps 1181 | }.from([]).to([ 1182 | :around_before, :before, 1183 | :around_before2, :before2, 1184 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1185 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1186 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1187 | :after2, :around_after2, 1188 | :around_before3, :before3, :call3, :after3, 1189 | :rollback3, 1190 | :rollback2c, 1191 | :rollback2b, 1192 | :rollback2a 1193 | ]) 1194 | end 1195 | 1196 | it "raises the error" do 1197 | expect { 1198 | organizer.call(context) 1199 | }.to raise_error("foo") 1200 | end 1201 | end 1202 | 1203 | context "when a deeply nested around hook fails early" do 1204 | let(:interactor4b) { 1205 | build_interactor do 1206 | around do |interactor| 1207 | context.fail! 1208 | context.steps << :around_before4b 1209 | interactor.call 1210 | context.steps << :around_after4b 1211 | end 1212 | 1213 | before do 1214 | context.steps << :before4b 1215 | end 1216 | 1217 | after do 1218 | context.steps << :after4b 1219 | end 1220 | 1221 | def call 1222 | context.steps << :call4b 1223 | end 1224 | 1225 | def rollback 1226 | context.steps << :rollback4b 1227 | end 1228 | end 1229 | } 1230 | 1231 | it "rolls back successfully called interactors" do 1232 | expect { 1233 | organizer.call(context) 1234 | }.to change { 1235 | context.steps 1236 | }.from([]).to([ 1237 | :around_before, :before, 1238 | :around_before2, :before2, 1239 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1240 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1241 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1242 | :after2, :around_after2, 1243 | :around_before3, :before3, :call3, :after3, :around_after3, 1244 | :around_before4, :before4, 1245 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1246 | :rollback4a, 1247 | :rollback3, 1248 | :rollback2c, 1249 | :rollback2b, 1250 | :rollback2a 1251 | ]) 1252 | end 1253 | end 1254 | 1255 | context "when a deeply nested around hook errors early" do 1256 | let(:interactor4b) { 1257 | build_interactor do 1258 | around do |interactor| 1259 | raise "foo" 1260 | end 1261 | 1262 | before do 1263 | context.steps << :before4b 1264 | end 1265 | 1266 | after do 1267 | context.steps << :after4b 1268 | end 1269 | 1270 | def call 1271 | context.steps << :call4b 1272 | end 1273 | 1274 | def rollback 1275 | context.steps << :rollback4b 1276 | end 1277 | end 1278 | } 1279 | 1280 | it "rolls back successfully called interactors" do 1281 | expect { 1282 | begin 1283 | organizer.call(context) 1284 | rescue 1285 | nil 1286 | end 1287 | }.to change { 1288 | context.steps 1289 | }.from([]).to([ 1290 | :around_before, :before, 1291 | :around_before2, :before2, 1292 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1293 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1294 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1295 | :after2, :around_after2, 1296 | :around_before3, :before3, :call3, :after3, :around_after3, 1297 | :around_before4, :before4, 1298 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1299 | :rollback4a, 1300 | :rollback3, 1301 | :rollback2c, 1302 | :rollback2b, 1303 | :rollback2a 1304 | ]) 1305 | end 1306 | 1307 | it "raises the error" do 1308 | expect { 1309 | organizer.call(context) 1310 | }.to raise_error("foo") 1311 | end 1312 | end 1313 | 1314 | context "when a deeply nested before hook fails" do 1315 | let(:interactor4b) { 1316 | build_interactor do 1317 | around do |interactor| 1318 | context.steps << :around_before4b 1319 | interactor.call 1320 | context.steps << :around_after4b 1321 | end 1322 | 1323 | before do 1324 | context.fail! 1325 | context.steps << :before4b 1326 | end 1327 | 1328 | after do 1329 | context.steps << :after4b 1330 | end 1331 | 1332 | def call 1333 | context.steps << :call4b 1334 | end 1335 | 1336 | def rollback 1337 | context.steps << :rollback4b 1338 | end 1339 | end 1340 | } 1341 | 1342 | it "rolls back successfully called interactors" do 1343 | expect { 1344 | organizer.call(context) 1345 | }.to change { 1346 | context.steps 1347 | }.from([]).to([ 1348 | :around_before, :before, 1349 | :around_before2, :before2, 1350 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1351 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1352 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1353 | :after2, :around_after2, 1354 | :around_before3, :before3, :call3, :after3, :around_after3, 1355 | :around_before4, :before4, 1356 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1357 | :around_before4b, 1358 | :rollback4a, 1359 | :rollback3, 1360 | :rollback2c, 1361 | :rollback2b, 1362 | :rollback2a 1363 | ]) 1364 | end 1365 | end 1366 | 1367 | context "when a deeply nested before hook errors" do 1368 | let(:interactor4b) { 1369 | build_interactor do 1370 | around do |interactor| 1371 | context.steps << :around_before4b 1372 | interactor.call 1373 | context.steps << :around_after4b 1374 | end 1375 | 1376 | before do 1377 | raise "foo" 1378 | end 1379 | 1380 | after do 1381 | context.steps << :after4b 1382 | end 1383 | 1384 | def call 1385 | context.steps << :call4b 1386 | end 1387 | 1388 | def rollback 1389 | context.steps << :rollback4b 1390 | end 1391 | end 1392 | } 1393 | 1394 | it "rolls back successfully called interactors" do 1395 | expect { 1396 | begin 1397 | organizer.call(context) 1398 | rescue 1399 | nil 1400 | end 1401 | }.to change { 1402 | context.steps 1403 | }.from([]).to([ 1404 | :around_before, :before, 1405 | :around_before2, :before2, 1406 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1407 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1408 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1409 | :after2, :around_after2, 1410 | :around_before3, :before3, :call3, :after3, :around_after3, 1411 | :around_before4, :before4, 1412 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1413 | :around_before4b, 1414 | :rollback4a, 1415 | :rollback3, 1416 | :rollback2c, 1417 | :rollback2b, 1418 | :rollback2a 1419 | ]) 1420 | end 1421 | 1422 | it "raises the error" do 1423 | expect { 1424 | organizer.call(context) 1425 | }.to raise_error("foo") 1426 | end 1427 | end 1428 | 1429 | context "when a deeply nested call fails" do 1430 | let(:interactor4b) { 1431 | build_interactor do 1432 | around do |interactor| 1433 | context.steps << :around_before4b 1434 | interactor.call 1435 | context.steps << :around_after4b 1436 | end 1437 | 1438 | before do 1439 | context.steps << :before4b 1440 | end 1441 | 1442 | after do 1443 | context.steps << :after4b 1444 | end 1445 | 1446 | def call 1447 | context.fail! 1448 | context.steps << :call4b 1449 | end 1450 | 1451 | def rollback 1452 | context.steps << :rollback4b 1453 | end 1454 | end 1455 | } 1456 | 1457 | it "rolls back successfully called interactors" do 1458 | expect { 1459 | organizer.call(context) 1460 | }.to change { 1461 | context.steps 1462 | }.from([]).to([ 1463 | :around_before, :before, 1464 | :around_before2, :before2, 1465 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1466 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1467 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1468 | :after2, :around_after2, 1469 | :around_before3, :before3, :call3, :after3, :around_after3, 1470 | :around_before4, :before4, 1471 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1472 | :around_before4b, :before4b, 1473 | :rollback4a, 1474 | :rollback3, 1475 | :rollback2c, 1476 | :rollback2b, 1477 | :rollback2a 1478 | ]) 1479 | end 1480 | end 1481 | 1482 | context "when a deeply nested call errors" do 1483 | let(:interactor4b) { 1484 | build_interactor do 1485 | around do |interactor| 1486 | context.steps << :around_before4b 1487 | interactor.call 1488 | context.steps << :around_after4b 1489 | end 1490 | 1491 | before do 1492 | context.steps << :before4b 1493 | end 1494 | 1495 | after do 1496 | context.steps << :after4b 1497 | end 1498 | 1499 | def call 1500 | raise "foo" 1501 | end 1502 | 1503 | def rollback 1504 | context.steps << :rollback4b 1505 | end 1506 | end 1507 | } 1508 | 1509 | it "rolls back successfully called interactors" do 1510 | expect { 1511 | begin 1512 | organizer.call(context) 1513 | rescue 1514 | nil 1515 | end 1516 | }.to change { 1517 | context.steps 1518 | }.from([]).to([ 1519 | :around_before, :before, 1520 | :around_before2, :before2, 1521 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1522 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1523 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1524 | :after2, :around_after2, 1525 | :around_before3, :before3, :call3, :after3, :around_after3, 1526 | :around_before4, :before4, 1527 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1528 | :around_before4b, :before4b, 1529 | :rollback4a, 1530 | :rollback3, 1531 | :rollback2c, 1532 | :rollback2b, 1533 | :rollback2a 1534 | ]) 1535 | end 1536 | 1537 | it "raises the error" do 1538 | expect { 1539 | organizer.call(context) 1540 | }.to raise_error("foo") 1541 | end 1542 | end 1543 | 1544 | context "when a deeply nested after hook fails" do 1545 | let(:interactor4b) { 1546 | build_interactor do 1547 | around do |interactor| 1548 | context.steps << :around_before4b 1549 | interactor.call 1550 | context.steps << :around_after4b 1551 | end 1552 | 1553 | before do 1554 | context.steps << :before4b 1555 | end 1556 | 1557 | after do 1558 | context.fail! 1559 | context.steps << :after4b 1560 | end 1561 | 1562 | def call 1563 | context.steps << :call4b 1564 | end 1565 | 1566 | def rollback 1567 | context.steps << :rollback4b 1568 | end 1569 | end 1570 | } 1571 | 1572 | it "rolls back successfully called interactors and the failed interactor" do 1573 | expect { 1574 | organizer.call(context) 1575 | }.to change { 1576 | context.steps 1577 | }.from([]).to([ 1578 | :around_before, :before, 1579 | :around_before2, :before2, 1580 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1581 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1582 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1583 | :after2, :around_after2, 1584 | :around_before3, :before3, :call3, :after3, :around_after3, 1585 | :around_before4, :before4, 1586 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1587 | :around_before4b, :before4b, :call4b, 1588 | :rollback4b, 1589 | :rollback4a, 1590 | :rollback3, 1591 | :rollback2c, 1592 | :rollback2b, 1593 | :rollback2a 1594 | ]) 1595 | end 1596 | end 1597 | 1598 | context "when a deeply nested after hook errors" do 1599 | let(:interactor4b) { 1600 | build_interactor do 1601 | around do |interactor| 1602 | context.steps << :around_before4b 1603 | interactor.call 1604 | context.steps << :around_after4b 1605 | end 1606 | 1607 | before do 1608 | context.steps << :before4b 1609 | end 1610 | 1611 | after do 1612 | raise "foo" 1613 | end 1614 | 1615 | def call 1616 | context.steps << :call4b 1617 | end 1618 | 1619 | def rollback 1620 | context.steps << :rollback4b 1621 | end 1622 | end 1623 | } 1624 | 1625 | it "rolls back successfully called interactors and the failed interactor" do 1626 | expect { 1627 | begin 1628 | organizer.call(context) 1629 | rescue 1630 | nil 1631 | end 1632 | }.to change { 1633 | context.steps 1634 | }.from([]).to([ 1635 | :around_before, :before, 1636 | :around_before2, :before2, 1637 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1638 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1639 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1640 | :after2, :around_after2, 1641 | :around_before3, :before3, :call3, :after3, :around_after3, 1642 | :around_before4, :before4, 1643 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1644 | :around_before4b, :before4b, :call4b, 1645 | :rollback4b, 1646 | :rollback4a, 1647 | :rollback3, 1648 | :rollback2c, 1649 | :rollback2b, 1650 | :rollback2a 1651 | ]) 1652 | end 1653 | 1654 | it "raises the error" do 1655 | expect { 1656 | organizer.call(context) 1657 | }.to raise_error("foo") 1658 | end 1659 | end 1660 | 1661 | context "when a deeply nested around hook fails late" do 1662 | let(:interactor4b) { 1663 | build_interactor do 1664 | around do |interactor| 1665 | context.steps << :around_before4b 1666 | interactor.call 1667 | context.fail! 1668 | context.steps << :around_after4b 1669 | end 1670 | 1671 | before do 1672 | context.steps << :before4b 1673 | end 1674 | 1675 | after do 1676 | context.steps << :after4b 1677 | end 1678 | 1679 | def call 1680 | context.steps << :call4b 1681 | end 1682 | 1683 | def rollback 1684 | context.steps << :rollback4b 1685 | end 1686 | end 1687 | } 1688 | 1689 | it "rolls back successfully called interactors and the failed interactor" do 1690 | expect { 1691 | organizer.call(context) 1692 | }.to change { 1693 | context.steps 1694 | }.from([]).to([ 1695 | :around_before, :before, 1696 | :around_before2, :before2, 1697 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1698 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1699 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1700 | :after2, :around_after2, 1701 | :around_before3, :before3, :call3, :after3, :around_after3, 1702 | :around_before4, :before4, 1703 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1704 | :around_before4b, :before4b, :call4b, :after4b, 1705 | :rollback4b, 1706 | :rollback4a, 1707 | :rollback3, 1708 | :rollback2c, 1709 | :rollback2b, 1710 | :rollback2a 1711 | ]) 1712 | end 1713 | end 1714 | 1715 | context "when a deeply nested around hook errors late" do 1716 | let(:interactor4b) { 1717 | build_interactor do 1718 | around do |interactor| 1719 | context.steps << :around_before4b 1720 | interactor.call 1721 | raise "foo" 1722 | end 1723 | 1724 | before do 1725 | context.steps << :before4b 1726 | end 1727 | 1728 | after do 1729 | context.steps << :after4b 1730 | end 1731 | 1732 | def call 1733 | context.steps << :call4b 1734 | end 1735 | 1736 | def rollback 1737 | context.steps << :rollback4b 1738 | end 1739 | end 1740 | } 1741 | 1742 | it "rolls back successfully called interactors and the failed interactor" do 1743 | expect { 1744 | begin 1745 | organizer.call(context) 1746 | rescue 1747 | nil 1748 | end 1749 | }.to change { 1750 | context.steps 1751 | }.from([]).to([ 1752 | :around_before, :before, 1753 | :around_before2, :before2, 1754 | :around_before2a, :before2a, :call2a, :after2a, :around_after2a, 1755 | :around_before2b, :before2b, :call2b, :after2b, :around_after2b, 1756 | :around_before2c, :before2c, :call2c, :after2c, :around_after2c, 1757 | :after2, :around_after2, 1758 | :around_before3, :before3, :call3, :after3, :around_after3, 1759 | :around_before4, :before4, 1760 | :around_before4a, :before4a, :call4a, :after4a, :around_after4a, 1761 | :around_before4b, :before4b, :call4b, :after4b, 1762 | :rollback4b, 1763 | :rollback4a, 1764 | :rollback3, 1765 | :rollback2c, 1766 | :rollback2b, 1767 | :rollback2a 1768 | ]) 1769 | end 1770 | 1771 | it "raises the error" do 1772 | expect { 1773 | organizer.call(context) 1774 | }.to raise_error("foo") 1775 | end 1776 | end 1777 | end 1778 | -------------------------------------------------------------------------------- /spec/interactor/context_spec.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | describe Context do 3 | describe ".build" do 4 | it "converts the given hash to a context" do 5 | context = Context.build(foo: "bar") 6 | 7 | expect(context).to be_a(Context) 8 | expect(context.foo).to eq("bar") 9 | end 10 | 11 | it "builds an empty context if no hash is given" do 12 | context = Context.build 13 | 14 | expect(context).to be_a(Context) 15 | expect(context.send(:table)).to eq({}) 16 | end 17 | 18 | it "doesn't affect the original hash" do 19 | hash = {foo: "bar"} 20 | context = Context.build(hash) 21 | 22 | expect(context).to be_a(Context) 23 | expect { 24 | context.foo = "baz" 25 | }.not_to change { 26 | hash[:foo] 27 | } 28 | end 29 | 30 | it "preserves an already built context" do 31 | context1 = Context.build(foo: "bar") 32 | context2 = Context.build(context1) 33 | 34 | expect(context2).to be_a(Context) 35 | expect { 36 | context2.foo = "baz" 37 | }.to change { 38 | context1.foo 39 | }.from("bar").to("baz") 40 | end 41 | end 42 | 43 | describe "#success?" do 44 | let(:context) { Context.build } 45 | 46 | it "is true by default" do 47 | expect(context.success?).to eq(true) 48 | end 49 | end 50 | 51 | describe "#failure?" do 52 | let(:context) { Context.build } 53 | 54 | it "is false by default" do 55 | expect(context.failure?).to eq(false) 56 | end 57 | end 58 | 59 | describe "#fail!" do 60 | let(:context) { Context.build(foo: "bar") } 61 | 62 | it "sets success to false" do 63 | expect { 64 | begin 65 | context.fail! 66 | rescue 67 | nil 68 | end 69 | }.to change { 70 | context.success? 71 | }.from(true).to(false) 72 | end 73 | 74 | it "sets failure to true" do 75 | expect { 76 | begin 77 | context.fail! 78 | rescue 79 | nil 80 | end 81 | }.to change { 82 | context.failure? 83 | }.from(false).to(true) 84 | end 85 | 86 | it "preserves failure" do 87 | begin 88 | context.fail! 89 | rescue 90 | nil 91 | end 92 | 93 | expect { 94 | begin 95 | context.fail! 96 | rescue 97 | nil 98 | end 99 | }.not_to change { 100 | context.failure? 101 | } 102 | end 103 | 104 | it "preserves the context" do 105 | expect { 106 | begin 107 | context.fail! 108 | rescue 109 | nil 110 | end 111 | }.not_to change { 112 | context.foo 113 | } 114 | end 115 | 116 | it "updates the context" do 117 | expect { 118 | begin 119 | context.fail!(foo: "baz") 120 | rescue 121 | nil 122 | end 123 | }.to change { 124 | context.foo 125 | }.from("bar").to("baz") 126 | end 127 | 128 | it "updates the context with a string key" do 129 | expect { 130 | begin 131 | context.fail!("foo" => "baz") 132 | rescue 133 | nil 134 | end 135 | }.to change { 136 | context.foo 137 | }.from("bar").to("baz") 138 | end 139 | 140 | it "raises failure" do 141 | expect { 142 | context.fail! 143 | }.to raise_error(Failure) 144 | end 145 | 146 | it "makes the context available from the failure" do 147 | begin 148 | context.fail! 149 | rescue Failure => error 150 | expect(error.context).to eq(context) 151 | end 152 | end 153 | end 154 | 155 | describe "#called!" do 156 | let(:context) { Context.build } 157 | let(:instance1) { double(:instance1) } 158 | let(:instance2) { double(:instance2) } 159 | 160 | it "appends to the internal list of called instances" do 161 | expect { 162 | context.called!(instance1) 163 | context.called!(instance2) 164 | }.to change { 165 | context._called 166 | }.from([]).to([instance1, instance2]) 167 | end 168 | end 169 | 170 | describe "#rollback!" do 171 | let(:context) { Context.build } 172 | let(:instance1) { double(:instance1) } 173 | let(:instance2) { double(:instance2) } 174 | 175 | before do 176 | allow(context).to receive(:_called) { [instance1, instance2] } 177 | end 178 | 179 | it "rolls back each instance in reverse order" do 180 | expect(instance2).to receive(:rollback).once.with(no_args).ordered 181 | expect(instance1).to receive(:rollback).once.with(no_args).ordered 182 | 183 | context.rollback! 184 | end 185 | 186 | it "ignores subsequent attempts" do 187 | expect(instance2).to receive(:rollback).once 188 | expect(instance1).to receive(:rollback).once 189 | 190 | context.rollback! 191 | context.rollback! 192 | end 193 | end 194 | 195 | describe "#_called" do 196 | let(:context) { Context.build } 197 | 198 | it "is empty by default" do 199 | expect(context._called).to eq([]) 200 | end 201 | end 202 | 203 | describe "#deconstruct_keys" do 204 | let(:context) { Context.build(foo: :bar) } 205 | 206 | let(:deconstructed) { context.deconstruct_keys([:foo, :success, :failure]) } 207 | 208 | it "deconstructs as hash pattern" do 209 | expect(deconstructed[:foo]).to eq(:bar) 210 | end 211 | 212 | it "includes success and failure" do 213 | expect(deconstructed[:success]).to eq(true) 214 | expect(deconstructed[:failure]).to eq(false) 215 | end 216 | end 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /spec/interactor/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | describe Hooks do 3 | describe "#with_hooks" do 4 | def build_hooked(&block) 5 | hooked = Class.new.send(:include, Interactor::Hooks) 6 | 7 | hooked.class_eval do 8 | attr_reader :steps 9 | 10 | def self.process 11 | new.tap(&:process).steps 12 | end 13 | 14 | def initialize 15 | @steps = [] 16 | end 17 | 18 | def process 19 | with_hooks { steps << :process } 20 | end 21 | end 22 | 23 | hooked.class_eval(&block) if block 24 | hooked 25 | end 26 | 27 | context "with an around hook method" do 28 | let(:hooked) { 29 | build_hooked do 30 | around :add_around_before_and_around_after 31 | 32 | private 33 | 34 | def add_around_before_and_around_after(hooked) 35 | steps << :around_before 36 | hooked.call 37 | steps << :around_after 38 | end 39 | end 40 | } 41 | 42 | it "runs the around hook method" do 43 | expect(hooked.process).to eq([ 44 | :around_before, 45 | :process, 46 | :around_after 47 | ]) 48 | end 49 | end 50 | 51 | context "with an around hook block" do 52 | let(:hooked) { 53 | build_hooked do 54 | around do |hooked| 55 | steps << :around_before 56 | hooked.call 57 | steps << :around_after 58 | end 59 | end 60 | } 61 | 62 | it "runs the around hook block" do 63 | expect(hooked.process).to eq([ 64 | :around_before, 65 | :process, 66 | :around_after 67 | ]) 68 | end 69 | end 70 | 71 | context "with an around hook method and block in one call" do 72 | let(:hooked) { 73 | build_hooked do 74 | around :add_around_before1_and_around_after1 do |hooked| 75 | steps << :around_before2 76 | hooked.call 77 | steps << :around_after2 78 | end 79 | 80 | private 81 | 82 | def add_around_before1_and_around_after1(hooked) 83 | steps << :around_before1 84 | hooked.call 85 | steps << :around_after1 86 | end 87 | end 88 | } 89 | 90 | it "runs the around hook method and block in order" do 91 | expect(hooked.process).to eq([ 92 | :around_before1, 93 | :around_before2, 94 | :process, 95 | :around_after2, 96 | :around_after1 97 | ]) 98 | end 99 | end 100 | 101 | context "with an around hook method and block in multiple calls" do 102 | let(:hooked) { 103 | build_hooked do 104 | around do |hooked| 105 | steps << :around_before1 106 | hooked.call 107 | steps << :around_after1 108 | end 109 | 110 | around :add_around_before2_and_around_after2 111 | 112 | private 113 | 114 | def add_around_before2_and_around_after2(hooked) 115 | steps << :around_before2 116 | hooked.call 117 | steps << :around_after2 118 | end 119 | end 120 | } 121 | 122 | it "runs the around hook block and method in order" do 123 | expect(hooked.process).to eq([ 124 | :around_before1, 125 | :around_before2, 126 | :process, 127 | :around_after2, 128 | :around_after1 129 | ]) 130 | end 131 | end 132 | 133 | context "with a before hook method" do 134 | let(:hooked) { 135 | build_hooked do 136 | before :add_before 137 | 138 | private 139 | 140 | def add_before 141 | steps << :before 142 | end 143 | end 144 | } 145 | 146 | it "runs the before hook method" do 147 | expect(hooked.process).to eq([ 148 | :before, 149 | :process 150 | ]) 151 | end 152 | end 153 | 154 | context "with a before hook block" do 155 | let(:hooked) { 156 | build_hooked do 157 | before do 158 | steps << :before 159 | end 160 | end 161 | } 162 | 163 | it "runs the before hook block" do 164 | expect(hooked.process).to eq([ 165 | :before, 166 | :process 167 | ]) 168 | end 169 | end 170 | 171 | context "with a before hook method and block in one call" do 172 | let(:hooked) { 173 | build_hooked do 174 | before :add_before1 do 175 | steps << :before2 176 | end 177 | 178 | private 179 | 180 | def add_before1 181 | steps << :before1 182 | end 183 | end 184 | } 185 | 186 | it "runs the before hook method and block in order" do 187 | expect(hooked.process).to eq([ 188 | :before1, 189 | :before2, 190 | :process 191 | ]) 192 | end 193 | end 194 | 195 | context "with a before hook method and block in multiple calls" do 196 | let(:hooked) { 197 | build_hooked do 198 | before do 199 | steps << :before1 200 | end 201 | 202 | before :add_before2 203 | 204 | private 205 | 206 | def add_before2 207 | steps << :before2 208 | end 209 | end 210 | } 211 | 212 | it "runs the before hook block and method in order" do 213 | expect(hooked.process).to eq([ 214 | :before1, 215 | :before2, 216 | :process 217 | ]) 218 | end 219 | end 220 | 221 | context "with an after hook method" do 222 | let(:hooked) { 223 | build_hooked do 224 | after :add_after 225 | 226 | private 227 | 228 | def add_after 229 | steps << :after 230 | end 231 | end 232 | } 233 | 234 | it "runs the after hook method" do 235 | expect(hooked.process).to eq([ 236 | :process, 237 | :after 238 | ]) 239 | end 240 | end 241 | 242 | context "with an after hook block" do 243 | let(:hooked) { 244 | build_hooked do 245 | after do 246 | steps << :after 247 | end 248 | end 249 | } 250 | 251 | it "runs the after hook block" do 252 | expect(hooked.process).to eq([ 253 | :process, 254 | :after 255 | ]) 256 | end 257 | end 258 | 259 | context "with an after hook method and block in one call" do 260 | let(:hooked) { 261 | build_hooked do 262 | after :add_after1 do 263 | steps << :after2 264 | end 265 | 266 | private 267 | 268 | def add_after1 269 | steps << :after1 270 | end 271 | end 272 | } 273 | 274 | it "runs the after hook method and block in order" do 275 | expect(hooked.process).to eq([ 276 | :process, 277 | :after2, 278 | :after1 279 | ]) 280 | end 281 | end 282 | 283 | context "with an after hook method and block in multiple calls" do 284 | let(:hooked) { 285 | build_hooked do 286 | after do 287 | steps << :after1 288 | end 289 | 290 | after :add_after2 291 | 292 | private 293 | 294 | def add_after2 295 | steps << :after2 296 | end 297 | end 298 | } 299 | 300 | it "runs the after hook block and method in order" do 301 | expect(hooked.process).to eq([ 302 | :process, 303 | :after2, 304 | :after1 305 | ]) 306 | end 307 | end 308 | 309 | context "with around, before and after hooks" do 310 | let(:hooked) { 311 | build_hooked do 312 | around do |hooked| 313 | steps << :around_before1 314 | hooked.call 315 | steps << :around_after1 316 | end 317 | 318 | around do |hooked| 319 | steps << :around_before2 320 | hooked.call 321 | steps << :around_after2 322 | end 323 | 324 | before do 325 | steps << :before1 326 | end 327 | 328 | before do 329 | steps << :before2 330 | end 331 | 332 | after do 333 | steps << :after1 334 | end 335 | 336 | after do 337 | steps << :after2 338 | end 339 | end 340 | } 341 | 342 | it "runs hooks in the proper order" do 343 | expect(hooked.process).to eq([ 344 | :around_before1, 345 | :around_before2, 346 | :before1, 347 | :before2, 348 | :process, 349 | :after2, 350 | :after1, 351 | :around_after2, 352 | :around_after1 353 | ]) 354 | end 355 | end 356 | end 357 | end 358 | end 359 | -------------------------------------------------------------------------------- /spec/interactor/organizer_spec.rb: -------------------------------------------------------------------------------- 1 | module Interactor 2 | describe Organizer do 3 | include_examples :lint 4 | 5 | let(:organizer) { Class.new.send(:include, Organizer) } 6 | 7 | describe ".organize" do 8 | let(:interactor2) { double(:interactor2) } 9 | let(:interactor3) { double(:interactor3) } 10 | 11 | it "sets interactors given class arguments" do 12 | expect { 13 | organizer.organize(interactor2, interactor3) 14 | }.to change { 15 | organizer.organized 16 | }.from([]).to([interactor2, interactor3]) 17 | end 18 | 19 | it "sets interactors given an array of classes" do 20 | expect { 21 | organizer.organize([interactor2, interactor3]) 22 | }.to change { 23 | organizer.organized 24 | }.from([]).to([interactor2, interactor3]) 25 | end 26 | end 27 | 28 | describe ".organized" do 29 | it "is empty by default" do 30 | expect(organizer.organized).to eq([]) 31 | end 32 | end 33 | 34 | describe "#call" do 35 | let(:instance) { organizer.new } 36 | let(:context) { double(:context) } 37 | let(:interactor2) { double(:interactor2) } 38 | let(:interactor3) { double(:interactor3) } 39 | let(:interactor4) { double(:interactor4) } 40 | 41 | before do 42 | allow(instance).to receive(:context) { context } 43 | allow(organizer).to receive(:organized) { 44 | [interactor2, interactor3, interactor4] 45 | } 46 | end 47 | 48 | it "calls each interactor in order with the context" do 49 | expect(interactor2).to receive(:call!).once.with(context).ordered 50 | expect(interactor3).to receive(:call!).once.with(context).ordered 51 | expect(interactor4).to receive(:call!).once.with(context).ordered 52 | 53 | instance.call 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/interactor_spec.rb: -------------------------------------------------------------------------------- 1 | describe Interactor do 2 | include_examples :lint 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["CODECLIMATE_REPO_TOKEN"] 2 | require "simplecov" 3 | SimpleCov.start 4 | end 5 | 6 | require "interactor" 7 | 8 | Dir[File.expand_path("../support/*.rb", __FILE__)].sort.each { |f| require f } 9 | -------------------------------------------------------------------------------- /spec/support/lint.rb: -------------------------------------------------------------------------------- 1 | shared_examples :lint do 2 | let(:interactor) { Class.new.send(:include, described_class) } 3 | 4 | describe ".call" do 5 | let(:context) { double(:context) } 6 | let(:instance) { double(:instance, context: context) } 7 | 8 | it "calls an instance with the given context" do 9 | expect(interactor).to receive(:new).once.with({foo: "bar"}) { instance } 10 | expect(instance).to receive(:run).once.with(no_args) 11 | 12 | expect(interactor.call(foo: "bar")).to eq(context) 13 | end 14 | 15 | it "provides a blank context if none is given" do 16 | expect(interactor).to receive(:new).once.with({}) { instance } 17 | expect(instance).to receive(:run).once.with(no_args) 18 | 19 | expect(interactor.call).to eq(context) 20 | end 21 | end 22 | 23 | describe ".call!" do 24 | let(:context) { double(:context) } 25 | let(:instance) { double(:instance, context: context) } 26 | 27 | it "calls an instance with the given context" do 28 | expect(interactor).to receive(:new).once.with({foo: "bar"}) { instance } 29 | expect(instance).to receive(:run!).once.with(no_args) 30 | 31 | expect(interactor.call!(foo: "bar")).to eq(context) 32 | end 33 | 34 | it "provides a blank context if none is given" do 35 | expect(interactor).to receive(:new).once.with({}) { instance } 36 | expect(instance).to receive(:run!).once.with(no_args) 37 | 38 | expect(interactor.call!).to eq(context) 39 | end 40 | end 41 | 42 | describe ".new" do 43 | let(:context) { double(:context) } 44 | 45 | it "initializes a context" do 46 | expect(Interactor::Context).to receive(:build).once.with({foo: "bar"}) { context } 47 | 48 | instance = interactor.new(foo: "bar") 49 | 50 | expect(instance).to be_a(interactor) 51 | expect(instance.context).to eq(context) 52 | end 53 | 54 | it "initializes a blank context if none is given" do 55 | expect(Interactor::Context).to receive(:build).once.with({}) { context } 56 | 57 | instance = interactor.new 58 | 59 | expect(instance).to be_a(interactor) 60 | expect(instance.context).to eq(context) 61 | end 62 | end 63 | 64 | describe "#run" do 65 | let(:instance) { interactor.new } 66 | 67 | it "runs the interactor" do 68 | expect(instance).to receive(:run!).once.with(no_args) 69 | 70 | instance.run 71 | end 72 | 73 | it "rescues failure with the same context" do 74 | expect(instance).to receive(:run!).and_raise(Interactor::Failure.new(instance.context)) 75 | 76 | expect { 77 | instance.run 78 | }.not_to raise_error 79 | end 80 | 81 | it "raises other failures" do 82 | expect(instance).to receive(:run!).and_raise(Interactor::Failure.new(Interactor::Context.new)) 83 | 84 | expect { 85 | instance.run 86 | }.to raise_error(Interactor::Failure) 87 | end 88 | 89 | it "raises other errors" do 90 | expect(instance).to receive(:run!).and_raise("foo") 91 | 92 | expect { 93 | instance.run 94 | }.to raise_error("foo") 95 | end 96 | end 97 | 98 | describe "#run!" do 99 | let(:instance) { interactor.new } 100 | 101 | it "calls the interactor" do 102 | expect(instance).to receive(:call).once.with(no_args) 103 | 104 | instance.run! 105 | end 106 | 107 | it "raises failure" do 108 | expect(instance).to receive(:run!).and_raise(Interactor::Failure) 109 | 110 | expect { 111 | instance.run! 112 | }.to raise_error(Interactor::Failure) 113 | end 114 | 115 | it "raises other errors" do 116 | expect(instance).to receive(:run!).and_raise("foo") 117 | 118 | expect { 119 | instance.run 120 | }.to raise_error("foo") 121 | end 122 | end 123 | 124 | describe "#call" do 125 | let(:instance) { interactor.new } 126 | 127 | it "exists" do 128 | expect(instance).to respond_to(:call) 129 | expect { instance.call }.not_to raise_error 130 | expect { instance.method(:call) }.not_to raise_error 131 | end 132 | end 133 | 134 | describe "#rollback" do 135 | let(:instance) { interactor.new } 136 | 137 | it "exists" do 138 | expect(instance).to respond_to(:rollback) 139 | expect { instance.rollback }.not_to raise_error 140 | expect { instance.method(:rollback) }.not_to raise_error 141 | end 142 | end 143 | end 144 | --------------------------------------------------------------------------------