README.md
# LightOperations
[![Gem Version](https://badge.fury.io/rb/light_operations.svg)](http://badge.fury.io/rb/light_operations)
[![Build Status](https://travis-ci.org/pniemczyk/light_operations.svg)](https://travis-ci.org/pniemczyk/light_operations)
[![Dependency Status](https://gemnasium.com/pniemczyk/light_operations.svg)](https://gemnasium.com/pniemczyk/light_operations)
[![Code Climate](https://codeclimate.com/github/pniemczyk/light_operations/badges/gpa.svg)](https://codeclimate.com/github/pniemczyk/light_operations)
When you want to have slim controllers or some logic with several operations
this gem could help you to have nice separated and clean code. CAN HELP YOU! :D
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'light_operations'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install light_operations
**Important latest version of gem > 1.2.x works only with ruby 2.x**
## How it works
Basically, this is a Container for business logic.
You can define dependencies during initialization and run with custom parameters.
When you define deferred actions on `success` and `fail` before operation execution is finished,
after execution one of those actions depend on for execution result will be executed.
Actions could be a block (Proc) or you could delegate execution to method another object,
by binding operation with the specific object with those methods.
You also could use operation as simple execution and check status by `success?` or `fail?` method
and then by using `subject` and `errors` method build your own logic to finish your result.
There are many possible use-cases where and how you could use operations.
You can build cascade of operations, use them one after the other,
use them recursively and a lot more.
Examples:
#### Simple
```ruby
require 'light_operations'
class CorrectNumber < LightOperations::Core
def execute(params)
params[:number] > 0 || fail!(:wrong_number)
end
end
op = CorrectNumber.new
p op.run(number: 0).success? # return false
p op.run(number: 0).false? # return true
p op.run(number: 1).success? # return true
p op.run(number: 1).false? # return false
```
#### With active_model
```ruby
require 'light_operations'
require 'active_model'
class Person
include ActiveModel::Model
attr_accessor :name, :age
validates_presence_of :name
end
class CreatePerson < LightOperations::Core
subject_name :person
def execute(params = {})
dependency(:repository).new(params).tap do |person|
person.valid?
end
end
end
class FakeController
def create(params = {})
create_operation.run(params)
end
def create_operation
@create_operation ||= CreatePerson.new(repository: Person).bind_with(self).on(success: :render_success, fail: :render_fail)
end
def render_success(operation)
person = operation.person
puts "name: #{person.name}"
end
def render_fail(operation)
person, errors = operation.subject, operation.errors
puts errors.as_json
puts "name: #{person.name}"
end
end
```
Class
```ruby
class MyOperation < LightOperations::Core
def execute(_params = nil)
dependency(:my_service) # when missing MissingDependency error will be raised
end
end
```
Initialization
```ruby
MyOperation.new(my_service: MyService.new)
```
You can add deferred actions for success and fail
```ruby
# 1
MyOperation.new.on_success { |operation| render :done, locals: { model: operation.subject } }
# 2
MyOperation.new.on(success: -> () { |operation| render :done, locals: { model: operation.subject } )
```
When you bind operation with another object you could delegate actions to bound object methods
```ruby
# 1
MyOperation.new.bind_with(self).on_success(:done)
# 2
MyOperation.new.bind_with(self).on(success: :done)
```
Execution method `#run` finalize actions execution
```ruby
MyOperation.new.bind_with(self).on(success: :done).run(params)
```
After execution operation hold execution state you could get back all info you need
- `#success?` => `true/false`
- `#fail?` => `true/false`
- `#subject?` => `success or fail object`
- `#errors` => `errors by default array but you can return any objec tou want`
Default usage
```ruby
operation.new(dependencies)
.on(success: :done, fail: :show_error)
.bind_with(self)
.run(params)
```
or
```ruby
operation.new(dependencies).tap do |op|
return op.run(params).success? ? op.subject : op.errors
end
```
#### success block or method receive operation as argument
##### operation.subject hold success object. You can use subject_name to create alias_method for subject
`(operation) -> { }`
or
```ruby
def success_method(operation)
...
end
```
#### fail block or method receive operation as argument
##### operation.subject, operation.errors hold failure object and errors. You can use subject_name to create alias_method for subject
`(operation) -> { }`
or
```ruby
def fail_method(operation)
...
end
```
## Usage
### Uses cases
#### Basic vote logic
Operation
```ruby
class ArticleVoteBumperOperation < LightOperations::Core
rescue_from ActiveRecord::RecordInvalid, with: :on_ar_error
def execute(_params = nil)
article = dependency(:article_model)
article.vote = article.vote.next
article.save!
{ success: true }
end
def on_ar_error(_exception)
fail!(vote: 'could not be updated!')
end
end
```
Controller
```ruby
class ArticleVotesController < ApplicationController
def up
operation = ArticleVoteBumperOperation.new(article_model: article)
response = operation.run.success? ? response.subject : response.errors
render :up, json: response
end
private
def article
Article.find(params.require(:id))
end
end
```
#### Basic recursive execution to collect news feeds from 2 sources
Operation
```ruby
class CollectFeedsOperation < LightOperations::Core
rescue_from Timeout::Error, with: :on_timeout
subject_name :news
def execute(params = {})
dependency(:http_client).get(params.fetch(:url)).body
end
def on_timeout
fail!
end
end
```
Controller
```ruby
class NewsFeedsController < ApplicationController
DEFAULT_NEWS_URL = 'http://rss.best_news.pl'
BACKUP_NEWS_URL = 'http://rss.not_so_bad_news.pl'
def news
collect_feeds_op
.bind_with(self)
.on(success: :display_news, fail: :second_attempt)
.run(url: DEFAULT_NEWS_URL)
end
private
def second_attempt(operation)
operation
.on_fail(:display_old_news)
.run(url: BACKUP_NEWS_URL)
end
def display_news(operation)
render :display_news, locals: { news: operation.news }
end
def display_old_news
end
def collect_feeds_op
@collect_feeds_op ||= CollectFeedsOperation.new(http_client: http_client)
end
def http_client
MyAwesomeHttpClient
end
end
```
#### Basic with active_model/active_record object
Operation
```ruby
class AddBookOperation < LightOperations::Core
subject_name :book
def execute(params = {})
dependency(:book_model).new(params).tap do |model|
model.valid? # this method automatically provide errors from model.errors
end
end
end
```
Controller
```ruby
class BooksController < ApplicationController
def index
render :index, locals: { collection: Book.all }
end
def new
render_book_form
end
def create
add_book_op
.bind_with(self)
.on(success: :book_created, fail: :render_book_form)
.run(permit_book_params)
end
private
def book_created(operation)
redirect_to :index, notice: "book #{operation.book.name} created"
end
def render_book_form(operation=nil)
book = operation ? operation.book : Book.new
render :new, locals: { book: book }
end
def add_book_op
@add_book_op ||= AddBookOperation.new(book_model: Book)
end
def permit_book_params
params.requre(:book)
end
end
```
#### Simple case when you want have user authorization
Operation
```ruby
class AuthOperation < LightOperations::Core
rescue_from AuthFail, with: :on_auth_error
subject_name :account
def execute(params = {})
dependency(:auth_service).login(login: login(params), password: password(params))
end
def on_auth_error(_exception)
fail!([login: 'unknown']) # or subject.errors.add(login: 'unknown')
end
def login(params)
params.fetch(:login)
end
def password(params)
params.fetch(:password)
end
end
```
Controller way #1
```ruby
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op
.bind_with(self)
.on_success(:create_session_with_dashbord_redirection)
.on_fail(:render_account_with_errors)
.run(params)
end
private
def create_session_with_dashbord_redirection(operation)
session_create_for(operation.account)
redirect_to :dashboard
end
def render_account_with_errors(operation)
render :new, locals: { account: operation.account }
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
```
Controller way #2
```ruby
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op
.on_success{ |op| create_session_with_dashbord_redirection(op.account) }
.on_fail { |op| render :new, locals: { account: op.account } }
.run(params)
end
private
def create_session_with_dashbord_redirection(account)
session_create_for(account)
redirect_to :dashboard
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
```
Controller way #3
```ruby
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op.on_success(&go_to_dashboard).on_fail(&go_to_login).run(params)
end
private
def go_to_dashboard
-> (op) do
session_create_for(op.account)
redirect_to :dashboard
end
end
def go_to_login
-> (op) { render :new, locals: { account: op.account } }
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
```
Register success and fails action is available by `#on` like:
```ruby
def create
auth_op.bind_with(self).on(success: :dashboard, fail: :show_error).run(params)
end
```
Operation have some helper methods (to improve recursive execution)
- `#clear!` => return operation to init state
- `#unbind!` => unbind binded object
- `#clear_subject_with_errors!` => clear subject and errors
When operation status is most important we can simply use `#success?` or `#fail?` on the executed operation
Errors are available by `#errors` after operation is executed
### Whats new in 1.2.x
New module LightOperations::Flow which gives very simple and easy way to create operation per action in the controller (tested on rails).
#### How it works:
include the module in a controller like this
```ruby
class AccountsController < VersionController
include LightOperations::Flow
operation :accounts, namespace: Operations, actions: [:create, :show]
def render_create(op)
render text: op.subject
end
def render_fail_create(op)
render text: op.errors # or if you want to show form use 'op.subject'
end
end
```
Now create operation class for account creation (components/operations/accounts/create.rb):
```ruby
module Operations
module Accounts
class Create < LightOperations::Core
rescue_from ActiveRecord::RecordInvalid, with: :invalid_record_handler
def execute(params:)
Account.create!(params.require(:account))
end
private
def invalid_record_handler(ex)
fail!(ex.record.errors)
end
end
end
end
```
add into `application.rb`
```ruby
config.autoload_paths += %W(
#{config.root}/app/components
)
```
But it is not all :D (operation params gives you a lot more)
```ruby
class AccountsController < VersionController
include LightOperations::Flow
operation(
:accounts, # top-level namespace
namespace: Operations, # Base namespace by default is Kernel
actions: [:create, :show], # those are operations executed by router
default_view: nil, # By changing this option you can have one method for render all successful operations for all actions.
view_prefix: 'render_', # By changing this you can have #view_create instead of #render_create
default_fail_view: nil, # By changing this option you can have one method for render all failed operations for all actions.
fail_view_prefix: 'render_fail_' # By changing this you can have #view_fail_create instead of #render_fail_create
end
```
This simple module should give you the power to create something like this:
```ruby
module Api
module V1
class AccountsController < VersionController
include LightOperations::Flow
skip_before_action :authorize, only: [:create, :password_reset]
operation :accounts,
namespace: Operations,
actions: [:create, :update, :show, :destroy, :password_reset],
default_fail_view: :render_error
private
def render_operation_error(op)
render json: op.errors, status: 422 # you can have status in operation if you want
end
def render_account(op)
render json: AccountOwnerSerializer.new(op.account), status: op.status
end
def render_no_content(_op)
render nothing: true, status: :no_content
end
alias_method :render_update, :render_account
alias_method :render_create, :render_account
alias_method :render_password_reset, :render_no_content
alias_method :render_destroy, :render_no_content
end
end
end
```
## Contributing
1. Fork it ( https://github.com/[my-github-username]/light_operations/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request