Skip to content

Instantly share code, notes, and snippets.

@mtortonesi
Created December 10, 2019 14:32
Show Gist options
  • Select an option

  • Save mtortonesi/e9ea2639e0089004de3aa27a555352d9 to your computer and use it in GitHub Desktop.

Select an option

Save mtortonesi/e9ea2639e0089004de3aa27a555352d9 to your computer and use it in GitHub Desktop.
Example of dry-validation and dry-transaction adoption within Rails 6
require_relative '../operations/create_actor'
require_relative '../operations/update_actor'
class ActorsController < ApplicationController
# GET /actors
# GET /actors.json
def index
# @actors = Actor.all
@actors = Actor.order(:name).page(params[:page])
end
# GET /actors/1
# GET /actors/1.json
def show
@actor = Actor.find(params[:id])
end
# GET /actors/new
def new
@actor = Actor.new
end
# GET /actors/1/edit
def edit
@actor = Actor.find(params[:id])
end
# POST /actors
# POST /actors.json
def create
# need to call .to_unsafe_h because dry-validation won't accept Rails's wacky params format
CreateActor.new.call(params.to_unsafe_h[:actor]) do |m|
m.success do |actor|
respond_to do |format|
format.html { redirect_to actor, notice: 'Actor was successfully created.' }
format.json { render :show, status: :created, location: actor }
end
end
m.failure do |errors|
@actor = Actor.new; @errors = errors
respond_to do |format|
format.html { render :new }
format.json { render json: @errors, status: :unprocessable_entity }
end
end
end
end
# PATCH/PUT /actors/1
# PATCH/PUT /actors/1.json
def update
# need to call .to_unsafe_h because dry-validation won't accept Rails's wacky params format
UpdateActor.new.call(params[:id], params.to_unsafe_h[:actor]) do |m|
m.success do |actor|
respond_to do |format|
format.html { redirect_to actor, notice: 'Actor was successfully created.' }
format.json { render :show, status: :ok, location: actor }
end
end
m.failure do |errors|
@actor = Actor.find(params[:id]);
@errors = errors
respond_to do |format|
format.html { render :new }
format.json { render json: @errors, status: :unprocessable_entity }
end
end
end
end
# DELETE /actors/1
# DELETE /actors/1.json
def destroy
# this action is very simple: no need to define a dedicated operation for it
Actor.destroy(params[:id])
respond_to do |format|
format.html { redirect_to actors_url, notice: 'Actor was successfully destroyed.' }
format.json { head :no_content }
end
end
end
require 'dry-transaction'
require_relative '../validators/actor_contract'
class CreateActor
include Dry::Transaction
step :validate
step :persist
private
def validate(input)
result = ActorContract.new.call(input)
if result.success?
Success(result.to_h)
else
Failure(result.errors(full: true))
end
end
def persist(input)
actor = Actor.new(input)
if actor.save
Success(actor)
else
Failure(OpenStruct.new(messages: [ "cannot persist actor" ]))
end
end
end
require 'dry-transaction'
require_relative '../validators/actor_contract'
class UpdateActor
include Dry::Transaction
step :validate
step :persist
private
def validate(input)
result = ActorContract.new.call(input[:actor])
if result.success?
Success(input)
else
Failure(result.errors(full: true))
end
end
def persist(input)
actor = Actor.find(input[:id])
if actor.update(input[:actor])
Success(actor)
else
Failure(OpenStruct.new(messages: [ "cannot persist actor" ]))
end
end
end
require 'dry-validation'
class ActorContract < Dry::Validation::Contract
params do
required(:name).filled(:string)
required(:dob).value(:date)
end
end
require 'test_helper'
class CreateActorTest < ActiveSupport::TestCase
def setup
@valid_input = { name: "Terence Hill", dob: "1939-03-29" }
@invalid_input = @valid_input.except(@valid_input.keys.sample)
end
test "it creates an actor in case of valid input" do
result = CreateActor.new.call(@valid_input)
assert result.success?
end
test "it should not create an actor given invalid input" do
result = CreateActor.new.call(@invalid_input)
assert result.failure?
end
test "it should return error messages in case of failure" do
result = CreateActor.new.call(@invalid_input)
assert result.failure.messages
end
end
require 'test_helper'
class UpdateActorTest < ActiveSupport::TestCase
def setup
@actor = actors(:one)
@valid_input = { name: "Terence Hill", dob: "1939-03-29" }
@invalid_input = @valid_input.except(@valid_input.keys.sample)
end
test "it updates an actor in case of valid input" do
result = UpdateActor.new.call(id: @actor.id, actor: @valid_input)
assert result.success?
end
test "it should not update an actor given invalid input" do
result = UpdateActor.new.call(id: @actor.id, actor: @invalid_input)
refute result.success?
end
test "it should return error messages in case of failure" do
result = UpdateActor.new.call(@invalid_input)
assert result.failure.messages
end
end
@mtortonesi
Copy link
Author

Dear @solnic,

thank you so very much for your kind suggestions. I refactored the code as follows.

However, note that by doing so I hit Issue 115, which was kind of hard to debug. (Rails is a complicated beast which, to use a kind euphemism, doesn't really go the extra mile to avoid breaking the principle of least surprise.) Anyway, I was able to get the code working by changing the single occurrence of EMPTY_ARRAY in dry-monads' do.rb to [].

app/operations/create_actor.rb:

require 'dry/monads'
require 'dry/monads/do'
require_relative '../validators/actor_contract'

class CreateActor
  include Dry::Monads[:result]
  include Dry::Monads::Do.for(:call)

  def call(params)
    valid_input = yield validate(params)
    create_actor(valid_input)
  end

  def validate(input)
    result = ActorContract.new.call(input)
    if result.success? 
      Success(result.to_h)
    else
      Failure(result.errors(full: true).messages)
    end
  end

  def create_actor(input)
    actor = Actor.new(input)
    if actor.save
      Success(actor)
    else
      Failure(actor.errors.full_messages)
    end
  end
end

app/operations/update_actor.rb:

require 'dry/monads'
require 'dry/monads/do'
require_relative '../validators/actor_contract'

class UpdateActor
  include Dry::Monads[:result]
  include Dry::Monads::Do.for(:call)

  def call(params)
    valid_input = yield validate(params)
    update_actor(valid_input)
  end

  def validate(input)
    result = ActorContract.new.call(input[:actor])
    if result.success? 
      Success(input)
    else
      Failure(result.errors(full: true).messages)
    end
  end

  def update_actor(input)
    actor = Actor.find(input[:id])
    if actor.update(input[:actor])
      Success(actor)
    else
      Failure(actor.errors.full_messages)
    end
  end
end

app/controllers/actors_controller.rb:

require_relative '../operations/create_actor'
require_relative '../operations/update_actor'


class ActorsController < ApplicationController
  # GET /actors
  # GET /actors.json
  def index
    # @actors = Actor.all
    @actors = Actor.order(:name).page(params[:page])
  end

  # GET /actors/1
  # GET /actors/1.json
  def show
    @actor = Actor.find(params[:id])
  end

  # GET /actors/new
  def new
    @actor = Actor.new
  end

  # GET /actors/1/edit
  def edit
    @actor = Actor.find(params[:id])
  end

  # POST /actors
  # POST /actors.json
  def create
    # need to call .to_unsafe_h because dry-validation won't accept Rails's wacky params format
    result = CreateActor.new.(params.to_unsafe_h[:actor])
    if result.success?
      actor = result.value!
      respond_to do |format|
        format.html { redirect_to actor, notice: 'Actor was successfully created.' }
        format.json { render :show, status: :created, location: actor }
      end
    else
      @actor = Actor.new
      @errors = result.failure
      respond_to do |format|
        format.html { render :new }
        format.json { render json: @errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /actors/1
  # PATCH/PUT /actors/1.json
  def update
    result = UpdateActor.new.(params.to_unsafe_h.symbolize_keys)
    if result.success?
      actor = result.value!
      respond_to do |format|
        format.html { redirect_to actor, notice: 'Actor was successfully updated.' }
        format.json { render :show, status: :ok, location: actor }
      end
    else
      @actor = Actor.find(params[:id]);
      @errors = result.failure
      respond_to do |format|
        format.html { render :new }
        format.json { render json: @errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /actors/1
  # DELETE /actors/1.json
  def destroy
    # questa è una action molto semplice: non è necessario definire un'operation per gestirla
    Actor.destroy(params[:id])

    respond_to do |format|
      format.html { redirect_to actors_url, notice: 'Actor was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment