Introduction

Info

Before dry-rails hits 1.0.0 it should be considered as beta software. Various usage patterns should emerge as more people try it out, so please do so and provide feedback!

dry-rails is the official dry-rb railtie for Ruby on Rails framework. It provides an application container using dry-system with additional features:

  • :safe_params - a small controller extension that adds the ability to define schemas for controller actions
  • :application_contract - sets up ApplicationContract class for you
  • :controller_helpers - convenient methods for working with the application container

Installation

To generate a new application skeleton with dry-rails installed, you can use the default template provided by the gem. Here's a minimalistic example:

rails new my_app -MOPCSJT --template https://raw.githubusercontent.com/dry-rb/dry-rails/master/templates/default.rb

If you already have a Rails application, simply add dry-rails to your Gemfile:

gem "dry-rails", "~> 0.1"

Overview

The railtie integrates dry-system with the Rails runtime by setting up a system container for you that works out of the box with no configuration required. This is based on a couple of conventions:

  1. The system container is defined as Container constant within your application namespace defined by config/application.rb. For example, if you generate a new rails application and call it blog, then the application namespace will be called Blog, and so your system container will be available as Blog::Container
  2. The auto-injection mixin is defined as Import under the application namespace too
  3. You can tweak the system container via an initializer, it can be called however you want, but the convention that we use is to call it config/initializers/system.rb
  4. Bootable components are expected to be found in config/system/*.rb files

The railtie supports code-reloading in development mode - Container, Import and boot files get reloaded upon every request (or when you manually reload! in the console).

Using auto-registration

Currently, the railtie does not make any assumptions about your directory/file structure. This means you are expected to specify where your components are located. Here's an example:

# config/initializers/system.rb
Dry::Rails.container do
  auto_register!("app/operations")
end

Then, in app/operations you can add your own classes and have them auto-registered and exposed via application container:

# app/operations/users/create
module Users
  class Create
  end
end

You can easily verify this using the console:

irb(main):001:0> MyApp::Container['users.create']
=> #<Users::Create:0x00007fa8f7c04f48>

Using Import mixin

The auto-injection mechanism is also set up for you automatically. Let's say you have a GitHub service that needs an HTTP client. The HTTP client will be part of your lib but the GitHub service will be part of your app. Here's how you could set it up:

# lib/my_app/http.rb
module MyApp
  class HTTP
    # some useful methods
  end
end

# app/services/github.rb
class Github
  include MyApp::Import[:http]

  # more useful methods
end

You can verify that Github has access to HTTP object in the console:

# bin/rails console

irb(main):001:0> MyApp::Container[:github]
=> #<Github:0x00007fb38c2fae30 @http=#<MyApp::HTTP:0x00007fb38c2fb150>>

Inflector

By default, the railtie registers ActiveSupport::Inflector as the default inflector. You can access it via container:

# bin/rails console

irb(main):007:0> MyApp::Container[:inflector].demodulize("MyApp::Container")
=> "Container"
Info

It is recommended to inject the inflector via the import module, instead of referring to the global ActiveSupport::Inflector constant. Otherwise you'll lose the ability to easily switch inflectors when you have a need, which can happen. Your future-self will be grateful.

Safe Params

Controllers can specify schemas for their actions and access safe params through safe_params helper. This is like strong_parameters but much more powerful as its powered by dry-schema.

Here's a simple example:

class UsersController < ApplicationController
  schema(:show, :edit) do
    required(:id).value(:integer)
  end

  before_action :set_user, only: %i[show edit]

  def show
    render :show, user: @user
  end

  def edit
    render :edit, user: @user
  end

  private

  def set_user
    @user = User.find(safe_params[:id])
  end
end

safe_params returns a dry-schema result object, you can use it to return a default error response, ie:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action do
    if safe_params && safe_params.failure?
      render(:error, errors: safe_params.errors.to_h) and throw(:abort)
    end
  end
end
Info

If you're curious about the throw(:abort) part check out this article about early returns from action callbacks.

ApplicationContract

The railtie gives you access to ApplicationContract class which is powered by dry-validation. You can define your own contract classes and simply inherit from ApplicationContract:

# lib/users/contracts/new.rb
module Users
  module Contracts
    class New < ApplicationContract
      # define the schema and rules
    end
  end
end

Controller Helpers

With :controller_helpers feature you get a couple of convenient methods that makes it easier to access container and its registered objects:

  • ApplicationController#container - a simple shortcut to YourApp::Container constant
  • ApplicationController#resolve - a shortcut that delegates resolve to the container

Here's a simple usage example how you could access an operation powered by dry-monads:

class UsersController < ApplicationController
  def create
    resolve("users.create").(safe_params[:user]) do |m|
      m.success do |user|
        render json: user
      end

      m.failure do |code, errors|
        render json: { code: code, errors: errors.to_h }, status: :unprocessable_entity
      end
    end
  end
end
Info

The railtie will soon provide monadic operations too!

Turning features on/off

By default all the features are enabled but you can cherry-pick them if you want. Let's say you only want to use safe_params and controller_helpers, in such case you can configure that in the initializer:

# config/initializers/system.rb
Dry::Rails.container do
  config.features = %i[
    safe_params
    controller_helpers
  ]
end

Learn more

The railtie simply puts together other dry-rb gems and make them work out-of-the-box in a typical Rails application. If you want to fully leverage the power of these tools, it is recommended to check out individual gem documentation pages:

  • dry-system - which is the backbone of Dry::Rails::Container, your application container
  • dry-schema - which gives you the safe params feature
  • dry-validation - which gives you the application contract feature

octocatEdit on GitHub