Basic usage
Defining a transaction with local operations
You can define a standalone transaction based around a single class and its own instance methods. Each instance method must accept an input argument and return an output wrapped in a Success
or Failure
:
require "dry/transaction"
class CreateUser
include Dry::Transaction
step :validate
step :create
private
def validate(input)
# returns Success(valid_data) or Failure(validation)
end
def create(input)
# returns Success(user)
end
end
Defining a transaction with an operations container
You can also define a transaction that relies upon operation objects in a container. Each operation must respond to #call(input)
.
The container will be checked for the operations using #key?
, and the operations will be resolved from the container via #[]
. For this example, we’ll use dry-container:
require "dry/core"
require "dry/transaction"
require "dry/transaction/operation"
module Users
class Validate
include Dry::Transaction::Operation
def call(input)
# returns Success(valid_data) or Failure(validation)
end
end
class Create
include Dry::Transaction::Operation
def call(input)
# returns Success(user)
end
end
end
class Container
extend Dry::Core::Container::Mixin
namespace "users" do
register "validate" do
Users::Validate.new
end
register "create" do
Users::Create.new
end
end
end
n.b. this is a small, contrived container setup. In a real app, you should consider using dry-system to make it easier to populate a container with your own objects.
Once you have a container, you can pass it to your transaction mixin and refer to the registered operations.
class CreateUser
include Dry::Transaction(container: Container)
step :validate, with: "users.validate"
step :create, with: "users.create"
end
Creating a reusable transaction module
You can create a reusable transaction module if you want to share configuration (i.e. container or step adapters) across multiple transaction classes.
module MyApp
Transaction = Dry::Transaction(container: Container)
end
class CreateUser
include MyApp::Transaction
# Operations will be resolved from the `Container` specified above
step :validate, with: "users.validate"
step :create, with: "users.create"
end
Calling a transaction
Calling a transaction will run its operations in their specified order, with the output of each operation becoming the input for the next.
create_user = CreateUser.new
create_user.call(name: "Jane", email: "jane@doe.com")
# => Success(#<User name="Jane", email="jane@doe.com">)
Each transaction returns a result value wrapped in a Success
or Failure
object, based on the output of its final step. You can handle these results (including errors arising from particular steps) with a match block:
create_user.call(name: "Jane", email: "jane@doe.com") do |m|
m.success do |user|
puts "Created user for #{user.name}!"
end
m.failure :validate do |validation|
# Runs only when the transaction fails on the :validate step
puts "Please provide a valid user."
end
m.failure do |error|
# Runs for any other failure
puts "Couldn’t create this user."
end
end
The match cases are executed in order. The first match wins and halts subsequent matching. The result from the match also becomes the method call’s return value.
Passing additional step arguments
You can pass additional arguments to step operations using #with_step_args
. Provide the operations as keys, and the arguments as an array. The arguments array will be splatted into the end of the operation’s arguments.
By using #with_step_args
to pass additional step arguments, you can include operations in a transaction with any sort of #call(input, *args)
interface, including keyword arguments.
class CreateUser
include Dry::Transaction(container: Container)
step :validate
step :create
step :notify
private
def validate(input)
# ...
end
def create(input, account_id:)
# ...
end
def notify(user, recipient)
# ...
end
end
create_user = CreateUser.new
create_user
.with_step_args(
create: [account_id: 123],
notify: ["foo@bar.com"],
)
.call(name: "Jane", email: "jane@doe.com")