Created
December 10, 2019 14:32
-
-
Save mtortonesi/e9ea2639e0089004de3aa27a555352d9 to your computer and use it in GitHub Desktop.
Example of dry-validation and dry-transaction adoption within Rails 6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'dry-validation' | |
class ActorContract < Dry::Validation::Contract | |
params do | |
required(:name).filled(:string) | |
required(:dob).value(:date) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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
hey, this looks good but you may want to tweak it to use dry-monad's
Do
notation instead of dry-transaction, because we're discontinuing its development. See more here dry-rb/dry-transaction#127One small suggestion I'd have as well is to remove
OpenStruct
usage. Its behavior is broken because it responds to anything and returnsnil
even when there's no corresponding value. I believe simply returning a hash would be OK.