Dry-rb ecosystem in Ruby on Rails project


Everytime then you start your new RoR based project first steps you are doing is a basic architecture, directory structure, code organization and gems which you decide to use in your project.

Ruby on Rails have a lot of tutorials, best practices and a lot of gems for code organization and abstraction.

One of the interesting approach is a https://dry-rb.org.

dry-rb is a collection of next-generation Ruby libraries.

dry-rb helps you write clear, flexible, and more maintainable Ruby code. Each dry-rb gem fulfils a common task, and together they make a powerful platform for any kind of Ruby application.

Why dry.rb?

Dry ecosystem use a lot of gems for different purposes.

You can read about each gem here http://dry-rb.org/gems/.

Let’s take a look to the all of them:

dry-auto_inject Container-agnostic constructor injection mixin

dry-configurable Thread-safe configuration mixin

dry-container Simple and thread-safe IoC container

dry-core A toolset of small support modules used throughout the dry-rb & rom-rb ecosystems

dry-equalizer Simple mixin providing equality methods

dry-events Standalone pub/sub API

dry-inflector Standalone inflections

dry-initializer DSL for defining initializer params and options

dry-logic Predicate logic with composable rules

dry-matcher Flexible, expressive pattern matching

dry-monads Useful, common monads in idiomatic Ruby

dry-struct Attribute DSL for struct-like objects

dry-system Organize your code into reusable components

dry-transaction Business transaction DSL

dry-types Flexible type system with many built-in types

dry-validation Powerful data validation based on predicate logic

dry-view Functional, standalone view rendering

dry-web-roda A simple web stack combining Roda with the dry-rb and rom-rb gems

Using with Rails

In this post I want to show you using two gems from this list:

Gemfile

gem 'dry-validation', '~> 0.11.1'
gem 'dry-transaction', '~> 0.10.2'

dry-validation

http://dry-rb.org/gems/dry-validation

Dry validation has a simple DSL to describe Schemas of your model and use predicates logic to implement validation logic.

Let’s take a look into QuestionSchema:

require 'dry-validation'

QuestionSchema = Dry::Validation.Schema do
  required(:title).filled
  required(:body).filled
end

We just require dry-validation and use .filled to mark field required.

You can read more at: http://dry-rb.org/gems/dry-validation/basics/predicate-logic/

dry-transaction

http://dry-rb.org/gems/dry-transaction

Dry transaction has a simple DSL to describe Schemas of your model and use predicates logic to implement validation logic.

Let’s take a look into CreateQuestion:

require 'dry-transaction'

class CreateQuestion
  include Dry::Transaction

  step :validate
  step :persist

  def validate(input)
    validation = QuestionSchema.(input)
    if validation.success?
      Success(input)
    else
      Failure(validation.errors)
    end
  end

  def persist(input)
    question = repo.create(input)
    Success(question)
  end

  private

  def repo
    QuestionRepository.new(ROM.env)
  end
end

We just require dry-transaction and include Dry::Transaction to turn everything on.

When we describe step attributes, those steps - like steps in databases transactions, in our example there are two steps and two methods for each of them: validate and persist, if everything going good, we return Success and if something fails - Failure.

Depending on the result, execution going to the next step, or cancel entire code execution.

You can read more at: http://dry-rb.org/gems/dry-transaction/basic-usage/

Controller

Let’s move into more Rails specific things, our contollers.

All what we need here - call for CreateQuestion transaction command and move execution flow depending on success or failure.

class QuestionsController < ApplicationController

  before_action :authorize, except: [:show, :index]

  before_action :load_searches, only: [:show, :new, :create]

  def index
    @questions = repo.query(params[:q], current_user)
    load_searches
  end

  def show
    @question = repo.by_id_with_answers_and_users(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    transaction = CreateQuestion.new
    transaction.(question_params.merge!({ user_id: @current_user.id })) do |result|
      result.success do |question|
        flash[:notice] = "Question has been created."
        redirect_to questions_path
      end

      result.failure :validate do |errors|
        @question = Question.new(question_params)
        @errors = errors
        flash[:alert] = "Question could not be created."
        render :new
      end
    end
  end

  private

  def question_params
    params.require(:question).permit(:title, :body, :q).to_h.symbolize_keys
  end


  def repo
    QuestionRepository.new(ROM.env)
  end
end

You can find interesting thing here - QuestionRepository - you can read about it in the next post.

Full project at my GitHub: https://github.com/maratgaliev/qboard.

Stay tuned!