{"id":13394879,"url":"https://github.com/collectiveidea/interactor","last_synced_at":"2025-05-14T07:08:03.784Z","repository":{"id":9715571,"uuid":"11670330","full_name":"collectiveidea/interactor","owner":"collectiveidea","description":"Interactor provides a common interface for performing complex user interactions.","archived":false,"fork":false,"pushed_at":"2025-01-02T03:56:32.000Z","size":183,"stargazers_count":3408,"open_issues_count":27,"forks_count":216,"subscribers_count":50,"default_branch":"master","last_synced_at":"2025-05-07T06:59:34.179Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/collectiveidea.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2013-07-25T20:32:44.000Z","updated_at":"2025-05-05T22:24:55.000Z","dependencies_parsed_at":"2022-08-04T08:30:24.987Z","dependency_job_id":"b6b78a8d-bb9e-44a7-8a25-641e95f57066","html_url":"https://github.com/collectiveidea/interactor","commit_stats":{"total_commits":162,"total_committers":22,"mean_commits":7.363636363636363,"dds":0.2407407407407407,"last_synced_commit":"7604c4b90dee3b8ab1fbdfd9ca294e367eac4449"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Finteractor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Finteractor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Finteractor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/collectiveidea%2Finteractor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/collectiveidea","download_url":"https://codeload.github.com/collectiveidea/interactor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254092652,"owners_count":22013290,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T17:01:34.920Z","updated_at":"2025-05-14T07:08:03.731Z","avatar_url":"https://github.com/collectiveidea.png","language":"Ruby","readme":"# Interactor\n\n[![Gem Version](https://img.shields.io/gem/v/interactor.svg)](http://rubygems.org/gems/interactor)\n[![Build Status](https://github.com/collectiveidea/interactor/actions/workflows/tests.yml/badge.svg)](https://github.com/collectiveidea/interactor/actions/workflows/tests.yml)\n[![Maintainability](https://img.shields.io/codeclimate/maintainability/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)\n[![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)\n[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)\n\n## Getting Started\n\nAdd Interactor to your Gemfile and `bundle install`.\n\n```ruby\ngem \"interactor\", \"~\u003e 3.0\"\n```\n\n## What is an Interactor?\n\nAn interactor is a simple, single-purpose object.\n\nInteractors are used to encapsulate your application's\n[business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor\nrepresents one thing that your application *does*.\n\n### Context\n\nAn interactor is given a *context*. The context contains everything the\ninteractor needs to do its work.\n\nWhen an interactor does its single purpose, it affects its given context.\n\n#### Adding to the Context\n\nAs an interactor runs it can add information to the context.\n\n```ruby\ncontext.user = user\n```\n\n#### Failing the Context\n\nWhen something goes wrong in your interactor, you can flag the context as\nfailed.\n\n```ruby\ncontext.fail!\n```\n\nWhen given a hash argument, the `fail!` method can also update the context. The\nfollowing are equivalent:\n\n```ruby\ncontext.error = \"Boom!\"\ncontext.fail!\n```\n\n```ruby\ncontext.fail!(error: \"Boom!\")\n```\n\nYou can ask a context if it's a failure:\n\n```ruby\ncontext.failure? # =\u003e false\ncontext.fail!\ncontext.failure? # =\u003e true\n```\n\nor if it's a success.\n\n```ruby\ncontext.success? # =\u003e true\ncontext.fail!\ncontext.success? # =\u003e false\n```\n\n#### Dealing with Failure\n\n`context.fail!` always throws an exception of type `Interactor::Failure`.\n\nNormally, 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.\n\nThis 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.\n\nSee *Interactors in the Controller*, below, for the recommended usage of `call` and `success?`.\n\n### Hooks\n\n#### Before Hooks\n\nSometimes an interactor needs to prepare its context before the interactor is\neven run. This can be done with before hooks on the interactor.\n\n```ruby\nbefore do\n  context.emails_sent = 0\nend\n```\n\nA symbol argument can also be given, rather than a block.\n\n```ruby\nbefore :zero_emails_sent\n\ndef zero_emails_sent\n  context.emails_sent = 0\nend\n```\n\n#### After Hooks\n\nInteractors can also perform teardown operations after the interactor instance\nis run.\n\n```ruby\nafter do\n  context.user.reload\nend\n```\n\nNB: After hooks are only run on success.  If the `fail!` method is called, the interactor's after hooks are not run.\n\n#### Around Hooks\n\nYou can also define around hooks in the same way as before or after hooks, using\neither a block or a symbol method name. The difference is that an around block\nor method accepts a single argument. Invoking the `call` method on that argument\nwill continue invocation of the interactor. For example, with a block:\n\n```ruby\naround do |interactor|\n  context.start_time = Time.now\n  interactor.call\n  context.finish_time = Time.now\nend\n```\n\nWith a method:\n\n```ruby\naround :time_execution\n\ndef time_execution(interactor)\n  context.start_time = Time.now\n  interactor.call\n  context.finish_time = Time.now\nend\n```\n\nNB: If the `fail!` method is called, all of the interactor's around hooks cease execution, and no code after `interactor.call` will be run.\n\n#### Hook Sequence\n\nBefore hooks are invoked in the order in which they were defined while after\nhooks are invoked in the opposite order. Around hooks are invoked outside of any\ndefined before and after hooks. For example:\n\n```ruby\naround do |interactor|\n  puts \"around before 1\"\n  interactor.call\n  puts \"around after 1\"\nend\n\naround do |interactor|\n  puts \"around before 2\"\n  interactor.call\n  puts \"around after 2\"\nend\n\nbefore do\n  puts \"before 1\"\nend\n\nbefore do\n  puts \"before 2\"\nend\n\nafter do\n  puts \"after 1\"\nend\n\nafter do\n  puts \"after 2\"\nend\n```\n\nwill output:\n\n```\naround before 1\naround before 2\nbefore 1\nbefore 2\nafter 2\nafter 1\naround after 2\naround after 1\n```\n\n#### Interactor Concerns\n\nAn interactor can define multiple before/after hooks, allowing common hooks to\nbe extracted into interactor concerns.\n\n```ruby\nmodule InteractorTimer\n  extend ActiveSupport::Concern\n\n  included do\n    around do |interactor|\n      context.start_time = Time.now\n      interactor.call\n      context.finish_time = Time.now\n    end\n  end\nend\n```\n\n### An Example Interactor\n\nYour application could use an interactor to authenticate a user.\n\n```ruby\nclass AuthenticateUser\n  include Interactor\n\n  def call\n    if user = User.authenticate(context.email, context.password)\n      context.user = user\n      context.token = user.secret_token\n    else\n      context.fail!(message: \"authenticate_user.failure\")\n    end\n  end\nend\n```\n\nTo define an interactor, simply create a class that includes the `Interactor`\nmodule and give it a `call` instance method. The interactor can access its\n`context` from within `call`.\n\n## Interactors in the Controller\n\nMost of the time, your application will use its interactors from its\ncontrollers. The following controller:\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  def create\n    if user = User.authenticate(session_params[:email], session_params[:password])\n      session[:user_token] = user.secret_token\n      redirect_to user\n    else\n      flash.now[:message] = \"Please try again.\"\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  private\n\n  def session_params\n    params.require(:session).permit(:email, :password)\n  end\nend\n```\n\ncan be refactored to:\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  def create\n    result = AuthenticateUser.call(session_params)\n\n    if result.success?\n      session[:user_token] = result.token\n      redirect_to result.user\n    else\n      flash.now[:message] = t(result.message)\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  private\n\n  def session_params\n    params.require(:session).permit(:email, :password)\n  end\nend\n```\n\nThe `call` class method is the proper way to invoke an interactor. The hash\nargument is converted to the interactor instance's context. The `call` instance\nmethod is invoked along with any hooks that the interactor might define.\nFinally, the context (along with any changes made to it) is returned.\n\n## When to Use an Interactor\n\nGiven the user authentication example, your controller may look like:\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  def create\n    result = AuthenticateUser.call(session_params)\n\n    if result.success?\n      session[:user_token] = result.token\n      redirect_to result.user\n    else\n      flash.now[:message] = t(result.message)\n      render :new\n    end\n  end\n\n  private\n\n  def session_params\n    params.require(:session).permit(:email, :password)\n  end\nend\n```\n\nFor such a simple use case, using an interactor can actually require *more*\ncode. So why use an interactor?\n\n### Clarity\n\n[We](http://collectiveidea.com) often use interactors right off the bat for all\nof our destructive actions (`POST`, `PUT` and `DELETE` requests) and since we\nput our interactors in `app/interactors`, a glance at that directory gives any\ndeveloper a quick understanding of everything the application *does*.\n\n```\n▾ app/\n  ▸ controllers/\n  ▸ helpers/\n  ▾ interactors/\n      authenticate_user.rb\n      cancel_account.rb\n      publish_post.rb\n      register_user.rb\n      remove_post.rb\n  ▸ mailers/\n  ▸ models/\n  ▸ views/\n```\n\n**TIP:** Name your interactors after your business logic, not your\nimplementation. `CancelAccount` will serve you better than `DestroyUser` as the\naccount cancellation interaction takes on more responsibility in the future.\n\n### The Future™\n\n**SPOILER ALERT:** Your use case won't *stay* so simple.\n\nIn [our](http://collectiveidea.com) experience, a simple task like\nauthenticating a user will eventually take on multiple responsibilities:\n\n* Welcoming back a user who hadn't logged in for a while\n* Prompting a user to update his or her password\n* Locking out a user in the case of too many failed attempts\n* Sending the lock-out email notification\n\nThe list goes on, and as that list grows, so does your controller. This is how\nfat controllers are born.\n\nIf instead you use an interactor right away, as responsibilities are added, your\ncontroller (and its tests) change very little or not at all. Choosing the right\nkind of interactor can also prevent simply shifting those added responsibilities\nto the interactor.\n\n## Kinds of Interactors\n\nThere are two kinds of interactors built into the Interactor library: basic\ninteractors and organizers.\n\n### Interactors\n\nA basic interactor is a class that includes `Interactor` and defines `call`.\n\n```ruby\nclass AuthenticateUser\n  include Interactor\n\n  def call\n    if user = User.authenticate(context.email, context.password)\n      context.user = user\n      context.token = user.secret_token\n    else\n      context.fail!(message: \"authenticate_user.failure\")\n    end\n  end\nend\n```\n\nBasic interactors are the building blocks. They are your application's\nsingle-purpose units of work.\n\n### Organizers\n\nAn organizer is an important variation on the basic interactor. Its single\npurpose is to run *other* interactors.\n\n```ruby\nclass PlaceOrder\n  include Interactor::Organizer\n\n  organize CreateOrder, ChargeCard, SendThankYou\nend\n```\n\nIn the controller, you can run the `PlaceOrder` organizer just like you would\nany other interactor:\n\n```ruby\nclass OrdersController \u003c ApplicationController\n  def create\n    result = PlaceOrder.call(order_params: order_params)\n\n    if result.success?\n      redirect_to result.order\n    else\n      @order = result.order\n      render :new\n    end\n  end\n\n  private\n\n  def order_params\n    params.require(:order).permit!\n  end\nend\n```\n\nThe organizer passes its context to the interactors that it organizes, one at a\ntime and in order. Each interactor may change that context before it's passed\nalong to the next interactor.\n\n#### Rollback\n\nIf any one of the organized interactors fails its context, the organizer stops.\nIf the `ChargeCard` interactor fails, `SendThankYou` is never called.\n\nIn addition, any interactors that had already run are given the chance to undo\nthemselves, in reverse order. Simply define the `rollback` method on your\ninteractors:\n\n```ruby\nclass CreateOrder\n  include Interactor\n\n  def call\n    order = Order.create(order_params)\n\n    if order.persisted?\n      context.order = order\n    else\n      context.fail!\n    end\n  end\n\n  def rollback\n    context.order.destroy\n  end\nend\n```\n\n**NOTE:** The interactor that fails is *not* rolled back. Because every\ninteractor should have a single purpose, there should be no need to clean up\nafter any failed interactor.\n\n## Testing Interactors\n\nWhen written correctly, an interactor is easy to test because it only *does* one\nthing. Take the following interactor:\n\n```ruby\nclass AuthenticateUser\n  include Interactor\n\n  def call\n    if user = User.authenticate(context.email, context.password)\n      context.user = user\n      context.token = user.secret_token\n    else\n      context.fail!(message: \"authenticate_user.failure\")\n    end\n  end\nend\n```\n\nYou can test just this interactor's single purpose and how it affects the\ncontext.\n\n```ruby\ndescribe AuthenticateUser do\n  subject(:context) { AuthenticateUser.call(email: \"john@example.com\", password: \"secret\") }\n\n  describe \".call\" do\n    context \"when given valid credentials\" do\n      let(:user) { double(:user, secret_token: \"token\") }\n\n      before do\n        allow(User).to receive(:authenticate).with(\"john@example.com\", \"secret\").and_return(user)\n      end\n\n      it \"succeeds\" do\n        expect(context).to be_a_success\n      end\n\n      it \"provides the user\" do\n        expect(context.user).to eq(user)\n      end\n\n      it \"provides the user's secret token\" do\n        expect(context.token).to eq(\"token\")\n      end\n    end\n\n    context \"when given invalid credentials\" do\n      before do\n        allow(User).to receive(:authenticate).with(\"john@example.com\", \"secret\").and_return(nil)\n      end\n\n      it \"fails\" do\n        expect(context).to be_a_failure\n      end\n\n      it \"provides a failure message\" do\n        expect(context.message).to be_present\n      end\n    end\n  end\nend\n```\n\n[We](http://collectiveidea.com) use RSpec but the same approach applies to any\ntesting framework.\n\n### Isolation\n\nYou may notice that we stub `User.authenticate` in our test rather than creating\nusers in the database. That's because our purpose in\n`spec/interactors/authenticate_user_spec.rb` is to test just the\n`AuthenticateUser` interactor. The `User.authenticate` method is put through its\nown paces in `spec/models/user_spec.rb`.\n\nIt's a good idea to define your own interfaces to your models. Doing so makes it\neasy to draw a line between which responsibilities belong to the interactor and\nwhich to the model. The `User.authenticate` method is a good, clear line.\nImagine the interactor otherwise:\n\n```ruby\nclass AuthenticateUser\n  include Interactor\n\n  def call\n    user = User.where(email: context.email).first\n\n    # Yuck!\n    if user \u0026\u0026 BCrypt::Password.new(user.password_digest) == context.password\n      context.user = user\n    else\n      context.fail!(message: \"authenticate_user.failure\")\n    end\n  end\nend\n```\n\nIt would be very difficult to test this interactor in isolation and even if you\ndid, as soon as you change your ORM or your encryption algorithm (both model\nconcerns), your interactors (business concerns) break.\n\n*Draw clear lines.*\n\n### Integration\n\nWhile it's important to test your interactors in isolation, it's just as\nimportant to write good integration or acceptance tests.\n\nOne of the pitfalls of testing in isolation is that when you stub a method, you\ncould be hiding the fact that the method is broken, has changed or doesn't even\nexist.\n\nWhen you write full-stack tests that tie all of the pieces together, you can be\nsure that your application's individual pieces are working together as expected.\nThat becomes even more important when you add a new layer to your code like\ninteractors.\n\n**TIP:** If you track your test coverage, try for 100% coverage *before*\nintegrations tests. Then keep writing integration tests until you sleep well at\nnight.\n\n### Controllers\n\nOne of the advantages of using interactors is how much they simplify controllers\nand their tests. Because you're testing your interactors thoroughly in isolation\nas well as in integration tests (right?), you can remove your business logic\nfrom your controller tests.\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  def create\n    result = AuthenticateUser.call(session_params)\n\n    if result.success?\n      session[:user_token] = result.token\n      redirect_to result.user\n    else\n      flash.now[:message] = t(result.message)\n      render :new\n    end\n  end\n\n  private\n\n  def session_params\n    params.require(:session).permit(:email, :password)\n  end\nend\n```\n\n```ruby\ndescribe SessionsController do\n  describe \"#create\" do\n    before do\n      expect(AuthenticateUser).to receive(:call).once.with(email: \"john@doe.com\", password: \"secret\").and_return(context)\n    end\n\n    context \"when successful\" do\n      let(:user) { double(:user, id: 1) }\n      let(:context) { double(:context, success?: true, user: user, token: \"token\") }\n\n      it \"saves the user's secret token in the session\" do\n        expect {\n          post :create, session: { email: \"john@doe.com\", password: \"secret\" }\n        }.to change {\n          session[:user_token]\n        }.from(nil).to(\"token\")\n      end\n\n      it \"redirects to the homepage\" do\n        response = post :create, session: { email: \"john@doe.com\", password: \"secret\" }\n\n        expect(response).to redirect_to(user_path(user))\n      end\n    end\n\n    context \"when unsuccessful\" do\n      let(:context) { double(:context, success?: false, message: \"message\") }\n\n      it \"sets a flash message\" do\n        expect {\n          post :create, session: { email: \"john@doe.com\", password: \"secret\" }\n        }.to change {\n          flash[:message]\n        }.from(nil).to(I18n.translate(\"message\"))\n      end\n\n      it \"renders the login form\" do\n        response = post :create, session: { email: \"john@doe.com\", password: \"secret\" }\n\n        expect(response).to render_template(:new)\n      end\n    end\n  end\nend\n```\n\nThis controller test will have to change very little during the life of the\napplication because all of the magic happens in the interactor.\n\n### Rails\n\n[We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We\nput our interactors in `app/interactors` and we name them as verbs:\n\n* `AddProductToCart`\n* `AuthenticateUser`\n* `PlaceOrder`\n* `RegisterUser`\n* `RemoveProductFromCart`\n\nSee: [Interactor Rails](https://github.com/collectiveidea/interactor-rails)\n\n## Contributions\n\nInteractor is open source and contributions from the community are encouraged!\nNo contribution is too small.\n\nSee Interactor's\n[contribution guidelines](CONTRIBUTING.md) for more information.\n\n## Thank You\n\nA very special thank you to [Attila Domokos](https://github.com/adomokos) for\nhis fantastic work on [LightService](https://github.com/adomokos/light-service).\nInteractor is inspired heavily by the concepts put to code by Attila.\n\nInteractor was born from a desire for a slightly simplified interface. We\nunderstand that this is a matter of personal preference, so please take a look\nat LightService as well!\n","funding_links":[],"categories":["Ruby","Abstraction","Business logic","Libraries"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcollectiveidea%2Finteractor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcollectiveidea%2Finteractor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcollectiveidea%2Finteractor/lists"}