As an experimental feature a "transaction" is available: datacaster defined as a class.
require 'datacaster'
class UserRegistration
include Datacaster::Transaction
perform do
steps(
transform(&prepare),
typecast,
with(:email, transform(&send_email))
)
end
define_steps do
def typecast = hash_schema(name: string, email: string)
end
def initialize(user_id = 123)
@user_id = user_id
end
def prepare(x)
@user_id ||= 123
x.to_h
end
def send_email(email)
{address: email, sent: true, id: @user_id}
end
end
UserRegistration.(name: 'John', email: '[email protected]')
# => Datacaster::ValidResult({:name=>"John", :email=>{:address=>"[email protected]", :result=>true, :id=>123}})
Transaction is just a class which includes Datacaster::Transaction
. Upon inclusion, all datacaster predefined methods are added as class methods.
Call .perform(a_caster)
or .perform { a_caster }
(where caster
is a Datacaster instance) to define transaction steps.
Block form perform { a_caster }
is used to defer definition of class methods (otherwise, they should've been written above perform
in the code file). Block is eventually executed in a context of the class.
Transaction instance will behave as a normal datacaster (i.e. a_caster
itself) with the following enhancements:
1. Transaction class has .call
method which will initialize instance (available only if #initialize
doesn't have required arguments) and pass arguments to the instance's #call
.
2. Runtime-context for casters used in a transaction is the transaction instance itself. You can call transaction instance methods and get/set transaction instance variables inside blocks of check { ... }
, cast { ... }
and all the other predefined datacaster methods. That's why @user_id
works in the example above.
3. Convenience method define_steps
is added, which is just a better looking class << self
.
4. If class method is not found, it is automatically converted (with class method_missing
) to deferred instance method call. In the example above, .prepare
class method is not defined. However, perform
block executes in a class context and tries to look that method up. Instead, proc ->(value) { self.perfrom(value) }
is returned – a deferred instance method call (which is passed as block to standard transform
datacaster).
Note that steps
is a predefined Datacaster method (which works as &
), and so is transform
and with
. They are not Transaction-specific enhancements.
An experimental addition to Datacaster convenient for the use in Transaction is cast_around
– a way to wrap a number of steps inside some kind of setup/rollback block, e.g. a database transaction.
class UserRegistration
include Datacaster::Transaction
perform do
steps(
run { prepare },
inside_transaction.around(
run { register },
run { create_account }
),
run { log }
)
end
define_steps do
def inside_transaction = cast_around do |value, inner|
puts "DB transaction started"
result = inner.(value)
puts "DB transaction ended"
result
end
end
def prepare
puts "Preparing"
end
def register
puts "User is registered"
end
def create_account
puts "Account has been created"
end
def log
puts "Creating log entry"
end
end
UserRegistration.('a user object')
# Preparing
# DB transaction started
# User is registered
# Account has been created
# DB transaction ended
# Creating log entry
# => #<Datacaster::ValidResult("a user object")>
As shown in the example, cast_around { |value, inner| ...}.around(*casters)
works in the following manner: it yields incoming value as the first argument (value
) and casters specified in .around(...)
part as the second argument (inner
) to the block given. Casters are automatically joined with steps
if there are more than one.
Block may call steps.(value)
to execute casters in a regular manner. Block must return a kind of Datacaster::Result
. steps.(...)
will always return Datacaster::Result
, so that result could be passed as a cast_around
result, as shown in the example.
Note that run
is a predefined Datacaster method, not specific to Transaction.