Dry.rb in Rails


dry.rb is a great collection of the “next-generation” libraries for your Ruby projects.

Of course, it’s not good for your next blog engine, but can be useful for big “enterprise” projects with complicated logic and data operations.

Let’s take a look for example for small API, which we use in our office for employees board, you can get all employees list as unauthenticated user, but to view full user’s profile, you need an account and JWT token. I would prefer to use some kind of CommandQuery Separation approach in this project.

So, let’s start.

We have a User model for example, and we use Devise for authentication.

class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :trackable, :validatable,
         :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
  has_many :employments
  has_many :projects, through: :employments
  scope :ordered, -> { order(updated_at: :desc) }
end

We also have a app/commands directory, where we place our commands.

Or base command will be base_command.rb, we inherit all commands from this class.

Nothing special here, just include Dry::Transaction, class method run with params and block, and basic method for error handling.

class BaseCommand
  include Dry::Transaction

  def self.run(params, &block)
    new.call(params, &block)
  end

  def error(error)
    { base: [error] }
  end
end

We have ONE command for ONE action, and we start from sign_up_command.rb.

Steps here work like a steps in transactions - we just go step by step, and make a rollback if some step fails.

Next interesting thing is a Right and Left constructors from http://dry-rb.org/gems/dry-monads.

Let’s check docs, nothing special:

The Either monad is useful to express a series of computations that might
return an error object with additional information.

The Either mixin has two type constructors: Right and Left. 

The Right can be thought of as "everything went right" and the Left is used when
"something has gone wrong".

And what we have in RegistrationsController ?

Pattern matching. Use the Either monad to model success and failure and match on the result.

def create
  Users::SignUpCommand.run(user_params) do |c|
    c.success do |user|
      sign_up(resource_name, user)
      render json: { user: user }
    end

    c.failure do |errors|
      render json: { errors: errors }, status: :unprocessable_entity
    end
  end
end

Ok, great. But what if we want to get list of all created users?

We can use some query object here as usual.

Same here, simple steps, we just query for users and paginate it.

class Users::MainQuery

  include Dry::Transaction
  
  PER_PAGE = 10

  step :users_scope
  step :paginate

  def self.index_query(params = {}, &block)
    new.call(params: params, &block)
  end

  def users_scope(params:)
    Right(users: User, params: params)
  end

  def paginate(users:, params:)
    Right(users.ordered.paginate(page: params[:page], per_page: PER_PAGE))
  end
  
end

Then just call index_query method. Simple to write. Simple to test. Simple to understand.

def index
  Users::MainQuery.index_query(params) do |q|
    q.success { |users| api_response(users) }
    q.failure { api_response([]) }
  end
end