README.md in interaktor-0.1.3 vs README.md in interaktor-0.1.4

- old
+ new

@@ -1,176 +1,182 @@ -This is a fork of Interactor. This README has not yet been updated to reflect that. +# Interaktor ---- +[![Gem Version](https://img.shields.io/gem/v/interaktor.svg)](http://rubygems.org/gems/interaktor) +[![Build Status](https://img.shields.io/travis/collectiveidea/interaktor/master.svg)](https://travis-ci.org/taylorthurlow/interaktor) -# Interactor +**Interaktor** is a fork of [Interaktor by collectiveidea](https://github.com/collectiveidea/interaktor). While Interaktor is still used by collectiveidea internally, communication and progress has been slow in adapting to pull requests and issues. This inactivity combined with my desire to dial back on the Interaktor's inherent permissivity led me to fork it and create Interaktor. -[![Gem Version](https://img.shields.io/gem/v/interactor.svg)](http://rubygems.org/gems/interactor) -[![Build Status](https://img.shields.io/travis/collectiveidea/interactor/master.svg)](https://travis-ci.org/collectiveidea/interactor) -[![Maintainability](https://img.shields.io/codeclimate/maintainability/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor) -[![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor) -[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) +Fundamentally, Interaktor is the same as Interactor, but with a small DSL which is used to define attributes passed into the interaktor, such as: -## Getting Started +- Required attributes +- Optional attributes +- Attributes required on interaktor failure +- Attributes required on interaktor success -Add Interactor to your Gemfile and `bundle install`. +## Getting started +Add `interaktor` to your Gemfile and `bundle install`. + ```ruby -gem "interactor", "~> 4.0" +gem "interaktor", "~> 0.1" ``` -## What is an Interactor? +## What is an interaktor? -An interactor is a simple, single-purpose object. +An interaktor is a simple, single-purpose object. -Interactors are used to encapsulate your application's -[business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor -represents one thing that your application *does*. +Interaktors are used to encapsulate your application's [business logic](http://en.wikipedia.org/wiki/Business_logic). Each interaktor represents one thing that your application _does_. -### Context +### Attributes -An interactor is given a *context*. The context contains everything the -interactor needs to do its work. +#### Input attributes -When an interactor does its single purpose, it affects its given context. +Depending on its definition, an interaktor may require attributes to be passed in when it is invoked. These attributes contain everything the interaktor needs to do its work. -#### Adding to the Context +You may define `required` or `optional` attributes. -As an interactor runs it can add information to the context. - ```ruby -context.user = user -``` +class CreateUser + include Interaktor -#### Failing the Context + required :name -When something goes wrong in your interactor, you can flag the context as -failed. + optional :email -```ruby -context.fail! -``` + def call + User.create!( + name: name, + email: email, + ) + end +end -When given a hash argument, the `fail!` method can also update the context. The -following are equivalent: +CreateUser.call(name: "Foo Bar") -```ruby -context.error = "Boom!" -context.fail! ``` -```ruby -context.fail!(error: "Boom!") -``` +#### Output attributes -You can ask a context if it's a failure: +Based on the outcome of the interaktor's work, we can require certain attributes. In the example below, we must succeed with a `user_id` attribute, and if we fail, we must provide an `error_messages` attribute. +The use of `#success!` allows you to early-return from an interaktor's work. If no `success` attribute is provided, and the `call` method finishes execution normally, then the interaktor is considered to be in a successful state. + ```ruby -context.failure? # => false -context.fail! -context.failure? # => true -``` +class CreateUser + include Interaktor -or if it's a success. + required :name -```ruby -context.success? # => true -context.fail! -context.success? # => false + success :user_id + + failure :error_messages + + def call + user = User.new(name: name) + + if user.save + success!(user_id: user.id) + else + fail!(error_messages: user.errors.full_messages) + end + end +end + +result = CreateUser.call(name: "Foo Bar") + +if result.success? + puts "The new user ID is: #{result.user_id}". +else + puts "Creating the user failed: #{result.error_messages.join(", ")}". +end ``` -#### Dealing with Failure +#### Dealing with sailure -`context.fail!` always throws an exception of type `Interactor::Failure`. +`context.fail!` always throws an exception of type `Interaktor::Failure`. -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. +Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interaktor using the class method `.call`, then checks the `#success?` method of the context. -This works because the `call` class method swallows exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions. +This works because the `call` class method swallows exceptions. When unit testing an interaktor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions. -See *Interactors in the Controller*, below, for the recommended usage of `call` and `success?`. +See _Interaktors in the controller_, below, for the recommended usage of `call` and `success?`. ### Hooks -#### Before Hooks +#### Before hooks -Sometimes an interactor needs to prepare its context before the interactor is -even run. This can be done with before hooks on the interactor. +Sometimes an interaktor needs to prepare its context before the interaktor is even run. This can be done with before hooks on the interaktor. ```ruby before do - context.emails_sent = 0 + # Do some stuff end ``` A symbol argument can also be given, rather than a block. ```ruby -before :zero_emails_sent +before :do_some_stuff -def zero_emails_sent - context.emails_sent = 0 +def do_some_stuff + # Do some stuff end ``` -#### After Hooks +#### After hooks -Interactors can also perform teardown operations after the interactor instance -is run. +Interaktors can also perform teardown operations after the interaktor instance +is run. They are only run on success. ```ruby after do context.user.reload end ``` -NB: After hooks are only run on success. If the `fail!` method is called, the interactor's after hooks are not run. +#### Around hooks -#### Around Hooks +You can also define around hooks in the same way as before or after hooks, using either a block or a symbol method name. The difference is that an around block or method accepts a single argument. Invoking the `call` method on that argument will continue invocation of the interaktor. For example, with a block: -You can also define around hooks in the same way as before or after hooks, using -either a block or a symbol method name. The difference is that an around block -or method accepts a single argument. Invoking the `call` method on that argument -will continue invocation of the interactor. For example, with a block: - ```ruby -around do |interactor| - context.start_time = Time.now - interactor.call - context.finish_time = Time.now +around do |interaktor| + # Do stuff before + interaktor.call + # Do stuff after end ``` With a method: ```ruby -around :time_execution +around :do_stuff_around -def time_execution(interactor) - context.start_time = Time.now - interactor.call - context.finish_time = Time.now +def do_stuff_around(interaktor) + # Do stuff before + interaktor.call + # Do stuff after end ``` -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. +If `#fail!` is called, any code defined in the hook after the call to the interaktor will not be run. -#### Hook Sequence +#### Hook sequence Before hooks are invoked in the order in which they were defined while after hooks are invoked in the opposite order. Around hooks are invoked outside of any defined before and after hooks. For example: ```ruby -around do |interactor| +around do |interaktor| puts "around before 1" - interactor.call + interaktor.call puts "around after 1" end -around do |interactor| +around do |interaktor| puts "around before 2" - interactor.call + interaktor.call puts "around after 2" end before do puts "before 1" @@ -200,193 +206,43 @@ after 1 around after 2 around after 1 ``` -#### Interactor Concerns +#### Interaktor concerns -An interactor can define multiple before/after hooks, allowing common hooks to -be extracted into interactor concerns. +An interaktor can define multiple before/after hooks, allowing common hooks to +be extracted into interaktor concerns. ```ruby -module InteractorTimer +module InteraktorDoStuff extend ActiveSupport::Concern included do - around do |interactor| - context.start_time = Time.now - interactor.call - context.finish_time = Time.now + around do |interaktor| + # Do stuff before + interaktor.call + # Do stuff after end end end ``` -### An Example Interactor +# All documentation below this line has not been updated to reflect the fork from Interactor. -Your application could use an interactor to authenticate a user. +## Kinds of interaktors -```ruby -class AuthenticateUser - include Interactor +There are two kinds of interaktors built into the Interaktor library: basic +interaktors and organizers. - def call - if user = User.authenticate(context.email, context.password) - context.user = user - context.token = user.secret_token - else - context.fail!(message: "authenticate_user.failure") - end - end -end -``` +### Interaktors -To define an interactor, simply create a class that includes the `Interactor` -module and give it a `call` instance method. The interactor can access its -`context` from within `call`. +A basic interaktor is a class that includes `Interaktor` and defines `call`. -## Interactors in the Controller - -Most of the time, your application will use its interactors from its -controllers. The following controller: - ```ruby -class SessionsController < ApplicationController - def create - if user = User.authenticate(session_params[:email], session_params[:password]) - session[:user_token] = user.secret_token - redirect_to user - else - flash.now[:message] = "Please try again." - render :new - end - end - - private - - def session_params - params.require(:session).permit(:email, :password) - end -end -``` - -can be refactored to: - -```ruby -class SessionsController < ApplicationController - def create - result = AuthenticateUser.call(session_params) - - if result.success? - session[:user_token] = result.token - redirect_to result.user - else - flash.now[:message] = t(result.message) - render :new - end - end - - private - - def session_params - params.require(:session).permit(:email, :password) - end -end -``` - -The `call` class method is the proper way to invoke an interactor. The hash -argument is converted to the interactor instance's context. The `call` instance -method is invoked along with any hooks that the interactor might define. -Finally, the context (along with any changes made to it) is returned. - -## When to Use an Interactor - -Given the user authentication example, your controller may look like: - -```ruby -class SessionsController < ApplicationController - def create - result = AuthenticateUser.call(session_params) - - if result.success? - session[:user_token] = result.token - redirect_to result.user - else - flash.now[:message] = t(result.message) - render :new - end - end - - private - - def session_params - params.require(:session).permit(:email, :password) - end -end -``` - -For such a simple use case, using an interactor can actually require *more* -code. So why use an interactor? - -### Clarity - -[We](http://collectiveidea.com) often use interactors right off the bat for all -of our destructive actions (`POST`, `PUT` and `DELETE` requests) and since we -put our interactors in `app/interactors`, a glance at that directory gives any -developer a quick understanding of everything the application *does*. - -``` -▾ app/ - ▸ controllers/ - ▸ helpers/ - ▾ interactors/ - authenticate_user.rb - cancel_account.rb - publish_post.rb - register_user.rb - remove_post.rb - ▸ mailers/ - ▸ models/ - ▸ views/ -``` - -**TIP:** Name your interactors after your business logic, not your -implementation. `CancelAccount` will serve you better than `DestroyUser` as the -account cancellation interaction takes on more responsibility in the future. - -### The Future™ - -**SPOILER ALERT:** Your use case won't *stay* so simple. - -In [our](http://collectiveidea.com) experience, a simple task like -authenticating a user will eventually take on multiple responsibilities: - -* Welcoming back a user who hadn't logged in for a while -* Prompting a user to update his or her password -* Locking out a user in the case of too many failed attempts -* Sending the lock-out email notification - -The list goes on, and as that list grows, so does your controller. This is how -fat controllers are born. - -If instead you use an interactor right away, as responsibilities are added, your -controller (and its tests) change very little or not at all. Choosing the right -kind of interactor can also prevent simply shifting those added responsibilities -to the interactor. - -## Kinds of Interactors - -There are two kinds of interactors built into the Interactor library: basic -interactors and organizers. - -### Interactors - -A basic interactor is a class that includes `Interactor` and defines `call`. - -```ruby class AuthenticateUser - include Interactor + include Interaktor def call if user = User.authenticate(context.email, context.password) context.user = user context.token = user.secret_token @@ -395,28 +251,28 @@ end end end ``` -Basic interactors are the building blocks. They are your application's +Basic interaktors are the building blocks. They are your application's single-purpose units of work. ### Organizers -An organizer is an important variation on the basic interactor. Its single -purpose is to run *other* interactors. +An organizer is an important variation on the basic interaktor. Its single +purpose is to run _other_ interaktors. ```ruby class PlaceOrder - include Interactor::Organizer + include Interaktor::Organizer organize CreateOrder, ChargeCard, SendThankYou end ``` In the controller, you can run the `PlaceOrder` organizer just like you would -any other interactor: +any other interaktor: ```ruby class OrdersController < ApplicationController def create result = PlaceOrder.call(order_params: order_params) @@ -435,26 +291,26 @@ params.require(:order).permit! end end ``` -The organizer passes its context to the interactors that it organizes, one at a -time and in order. Each interactor may change that context before it's passed -along to the next interactor. +The organizer passes its context to the interaktors that it organizes, one at a +time and in order. Each interaktor may change that context before it's passed +along to the next interaktor. #### Rollback -If any one of the organized interactors fails its context, the organizer stops. -If the `ChargeCard` interactor fails, `SendThankYou` is never called. +If any one of the organized interaktors fails its context, the organizer stops. +If the `ChargeCard` interaktor fails, `SendThankYou` is never called. -In addition, any interactors that had already run are given the chance to undo +In addition, any interaktors that had already run are given the chance to undo themselves, in reverse order. Simply define the `rollback` method on your -interactors: +interaktors: ```ruby class CreateOrder - include Interactor + include Interaktor def call order = Order.create(order_params) if order.persisted? @@ -468,22 +324,22 @@ context.order.destroy end end ``` -**NOTE:** The interactor that fails is *not* rolled back. Because every -interactor should have a single purpose, there should be no need to clean up -after any failed interactor. +**NOTE:** The interaktor that fails is _not_ rolled back. Because every +interaktor should have a single purpose, there should be no need to clean up +after any failed interaktor. -## Testing Interactors +## Testing interaktors -When written correctly, an interactor is easy to test because it only *does* one -thing. Take the following interactor: +When written correctly, an interaktor is easy to test because it only _does_ one +thing. Take the following interaktor: ```ruby class AuthenticateUser - include Interactor + include Interaktor def call if user = User.authenticate(context.email, context.password) context.user = user context.token = user.secret_token @@ -492,11 +348,11 @@ end end end ``` -You can test just this interactor's single purpose and how it affects the +You can test just this interaktor's single purpose and how it affects the context. ```ruby describe AuthenticateUser do subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") } @@ -544,22 +400,22 @@ ### Isolation You may notice that we stub `User.authenticate` in our test rather than creating users in the database. That's because our purpose in -`spec/interactors/authenticate_user_spec.rb` is to test just the -`AuthenticateUser` interactor. The `User.authenticate` method is put through its +`spec/interaktors/authenticate_user_spec.rb` is to test just the +`AuthenticateUser` interaktor. The `User.authenticate` method is put through its own paces in `spec/models/user_spec.rb`. It's a good idea to define your own interfaces to your models. Doing so makes it -easy to draw a line between which responsibilities belong to the interactor and +easy to draw a line between which responsibilities belong to the interaktor and which to the model. The `User.authenticate` method is a good, clear line. -Imagine the interactor otherwise: +Imagine the interaktor otherwise: ```ruby class AuthenticateUser - include Interactor + include Interaktor def call user = User.where(email: context.email).first # Yuck! @@ -570,38 +426,38 @@ end end end ``` -It would be very difficult to test this interactor in isolation and even if you +It would be very difficult to test this interaktor in isolation and even if you did, as soon as you change your ORM or your encryption algorithm (both model -concerns), your interactors (business concerns) break. +concerns), your interaktors (business concerns) break. -*Draw clear lines.* +_Draw clear lines._ ### Integration -While it's important to test your interactors in isolation, it's just as +While it's important to test your interaktors in isolation, it's just as important to write good integration or acceptance tests. One of the pitfalls of testing in isolation is that when you stub a method, you could be hiding the fact that the method is broken, has changed or doesn't even exist. When you write full-stack tests that tie all of the pieces together, you can be sure that your application's individual pieces are working together as expected. That becomes even more important when you add a new layer to your code like -interactors. +interaktors. -**TIP:** If you track your test coverage, try for 100% coverage *before* +**TIP:** If you track your test coverage, try for 100% coverage _before_ integrations tests. Then keep writing integration tests until you sleep well at night. ### Controllers -One of the advantages of using interactors is how much they simplify controllers -and their tests. Because you're testing your interactors thoroughly in isolation +One of the advantages of using interaktors is how much they simplify controllers +and their tests. Because you're testing your interaktors thoroughly in isolation as well as in integration tests (right?), you can remove your business logic from your controller tests. ```ruby class SessionsController < ApplicationController @@ -671,37 +527,37 @@ end end ``` This controller test will have to change very little during the life of the -application because all of the magic happens in the interactor. +application because all of the magic happens in the interaktor. ### Rails -[We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We -put our interactors in `app/interactors` and we name them as verbs: +[We](http://collectiveidea.com) love Rails, and we use Interaktor with Rails. We +put our interaktors in `app/interaktors` and we name them as verbs: -* `AddProductToCart` -* `AuthenticateUser` -* `PlaceOrder` -* `RegisterUser` -* `RemoveProductFromCart` +- `AddProductToCart` +- `AuthenticateUser` +- `PlaceOrder` +- `RegisterUser` +- `RemoveProductFromCart` -See: [Interactor Rails](https://github.com/collectiveidea/interactor-rails) +See: [Interaktor Rails](https://github.com/collectiveidea/interaktor-rails) ## Contributions -Interactor is open source and contributions from the community are encouraged! +Interaktor is open source and contributions from the community are encouraged! No contribution is too small. -See Interactor's +See Interaktor's [contribution guidelines](CONTRIBUTING.md) for more information. ## Thank You A very special thank you to [Attila Domokos](https://github.com/adomokos) for his fantastic work on [LightService](https://github.com/adomokos/light-service). -Interactor is inspired heavily by the concepts put to code by Attila. +Interaktor is inspired heavily by the concepts put to code by Attila. -Interactor was born from a desire for a slightly simplified interface. We +Interaktor was born from a desire for a slightly simplified interface. We understand that this is a matter of personal preference, so please take a look at LightService as well!