Operation API
Last updated 29 August 2017 trailblazer v2.0 v1.1This document describes Trailblazer’s Operation
API.
The generic implementation can be found in the trailblazer-operation gem. This gem only provides the pipe and dependency handling.
Higher-level abstractions, such as form object or policy integration, are implemented in the trailblazer gem.
Invocation
An operation is designed like a function and it can only be invoked in one way: via Operation::call
.
Song::Create.call( name: "Roxanne" )
Ruby allows a shorthand for this which is commonly used throughout Trailblazer.
Song::Create.( name: "Roxanne" )
The absence of a method name here is per design: this object does only one thing, and hence what it does is reflected in the class name.
Running an operation will always return its → result object. It is up to you to interpret the content of it or push data onto the result object during the operation’s cycle.
result = Create.call( name: "Roxanne" )
result["model"] #=> #<Song name: "Roxanne">
Invocation: Dependencies
Dependencies other than the params input, such as a current user, are passed via the second argument.
result = Create.( { title: "Roxanne" }, "current_user" => current_user )
External dependencies will be accessible via options
in every step.
class Create < Trailblazer::Operation
step Model( Song, :new )
step :assign_current_user!
# ..
def assign_current_user!(options)
options["model"].created_by = options["current_user"]
end
end
They are also readable in the result.
result["current_user"] #=> #<User name="Ema">
result["model"] #=> #<Song id=nil, title=nil, created_by=#<User name="Ema">>
Keep in mind that there is no global state in Trailblazer, anything you need in the operation has to be injected via the second call
argument. This also applies to tests.
Flow Control
The operation’s sole purpose is to define the pipe with its steps that are executed when the operation is run. While traversing the pipe, each step orchestrates all necessary stakeholders like policies, contracts, models and callbacks.
The flow of an operation is defined by a two-tracked pipeline.

class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step :assign_current_user!
step Contract::Build( constant: MyContract )
step Contract::Validate()
failure :log_error!
step Contract::Persist()
def log_error!(options)
# ..
end
def assign_current_user!(options)
options["model"].created_by =
options["current_user"]
end
end
Per default, the right track will be run from top to bottom. If an error occurs, it will deviate to the left track and continue executing error handler steps on this track.
The flow pipetree is a mix of the Either
monad and “Railway-oriented programming”, but not entirely the same.
The following high-level API is available.
-
step
adds a step to the right track. If its return value isfalsey
, the pipe deviates to the left track. Can be called with macros, which will run their own insertion logic. -
success
always add step to the right. The return value is ignored. -
failure
always add step to the left for error handling. The return value is ignored.
Flow Control: Outcome
If the operation ends on the right track, the result object will return true on success?
.
result = Song::Create.({ title: "The Feeling Is Alright" }, "current_user": current_user)
result.success? #=> true
Otherwise, when the run ends on the left track, failure?
will return true.
result = Song::Create.({ })
result.success? #=> false
result.failure? #=> true
Incredible, we know.
Flow Control: Step
The step
method adds your step to the right track. The return value decides about track deviation.
class Create < Trailblazer::Operation
step :model!
def model!(options, **)
options["model"] = Song.new # return value evals to true.
end
end
The return value of model!
is evaluated.
Since the above example will always return something “truthy”, the pipe will stay on the right track after model!
.
However, if the step returns falsey
, the pipe will change to the left track.
class Update < Trailblazer::Operation
step :model!
def model!(options, params:, **)
options["model"] = Song.find_by(params[:id]) # might return false!
end
end
In the above example, it deviates to the left should the respective model not be found.
When adding step macros with step
, the behavior changes a bit. Macros can command step
to internally use other operators to attach their step(s).
class Create < Trailblazer::Operation
step Model( Song, :find_by )
end
However, most macro will internally use step
, too. Note that some macros, such as Contract::Validate
might add several steps in a row.
Flow Control: Success
If you don’t care about the result, and want to stay on the right track, use success
.
class Update < Trailblazer::Operation
success :model!
def model!(options, params:, **)
options["model"] = Song.find_by(params[:id]) # return value ignored!
end
end
Here, if model!
returns false
or nil
, the pipe stays on right track.
Flow Control: Failure
Error handlers on the left track can be added with failure
.
class Create < Trailblazer::Operation
step :model!
failure :error!
# ...
def error!(options, params:, **)
options["result.model"] = "Something went wrong with ID #{params[:id]}!"
end
end
Just as in right-tracked steps, you may add failure information to the result object that you want to communicate to the caller.
def error!(options, params:, **)
options["result.model"] = "Something went wrong with ID #{params[:id]}!"
end
Note that you can add as many error handlers as you want, at any position in the pipe. They will be executed in that order, just as it works on the right track.
Flow Control: Fail Fast Option
If you don’t want left track steps to be executed after a specific step, use the :fail_fast
option.
class Update < Trailblazer::Operation
step Model( Song, :find_by )
failure :abort!, fail_fast: true
step Contract::Build( constant: MyContract )
step Contract::Validate( )
failure :handle_invalid_contract! # won't be executed if #abort! is executed.
def abort!(options, params:, **)
options["result.model.song"] = "Something went wrong with ID #{params[:id]}!"
end
# ..
end
This will not execute any failure
steps after abort!
.
result = Update.(id: 1)
result["result.model.song"] #=> "Something went wrong with ID 1!"
Note that this option in combination with failure
will always fail fast once its reached, regardless of the step’s return value.
:fail_fast
also works with step
.
class Update < Trailblazer::Operation
step :empty_id?, fail_fast: true
step Model( Song, :find_by )
failure :handle_empty_db! # won't be executed if #empty_id? returns falsey.
def empty_id?(options, params:, **)
params[:id] # returns false if :id missing.
end
end
Here, if step
returns a falsey value, the rest of the pipe is skipped, returning a failed result. Again, this will not execute any failure
steps after :empty_id?
.
result = Update.({ id: nil })
result.failure? #=> true
result["model"] #=> nil
Flow Control: Fail Fast
Instead of hardcoding the flow behavior you can have a dynamic skipping of left track steps based on some condition. This works with the fail_fast!
method.
class Update < Trailblazer::Operation
step :filter_params! # emits fail_fast!
step Model( Song, :find_by )
failure :handle_fail!
def filter_params!(options, params:, **)
unless params[:id]
options["result.params"] = "No ID in params!"
return Railway.fail_fast!
end
end
def handle_fail!(options, **)
options["my.status"] = "Broken!"
end
end
This will not execute any steps on either track, but will result in a failed operation.
result = Update.(id: 1)
result["result.params"] #=> "No ID in params!"
result["my.status"] #=> nil
Note that you have to return Railway.fail_fast!
from the track. You can use this signal from any step, e.g. step
or failure
.
Step Implementation
A step can be added via step
, success
and failure
. It can be implemented as an instance method.
class Create < Trailblazer::Operation
step :model!
def model!(options, **)
options["model"] = Song.new
end
end
Note that you can use modules to share steps across operations.
Step Implementation: Lambda
Or as a proc.
class Create < Trailblazer::Operation
step ->(options, **) { options["model"] = Song.new }
end
Step Implementation: Callable
Or, for more reusability, as a Callable
.
class MyModel
extend Uber::Callable
def self.call(options, **)
options["model"] = Song.new
end
end
Simply pass the class (or stateless instance) to the step operator.
class Create < Trailblazer::Operation
step MyModel
end
Step Arguments
Each step receives the context object as a positional argument. All runtime data is also passed as keyword arguments to the step. Whether method, proc or callable object, use the positional options to write, and make use of kw args wherever possible.
For example, you can use kw args with a proc.
class Create < Trailblazer::Operation
step ->(options, params:, current_user:, **) { }
end
Or with an instance method.
class Create < Trailblazer::Operation
step :setup!
def setup!(options, params:, current_user:, **)
# ...
end
end
The first options
is the positional argument and ideal to write new data onto the context. This is the mutable part which transports mutable state from one step to the next.
After that, only extract the parameters you need (such as params:
). Any unspecified keyword arguments can be ignored using **
.
Keyword arguments work fine in Ruby 2.1 and >=2.2.3. They are broken in Ruby 2.2.2 and have a to-be-confirmed unexpected behavior in 2.0.
Result Object
Calling an operation returns a Result
object. Sometimes we also called it a context object as it’s available throughout the call. It is passed from step to step, and the steps can read and write to it.
Consider the following operation.
class Song::Create < Trailblazer::Operation
step :model!
step :assign!
step :validate!
def model!(options, current_user:, **)
options["model"] = Song.new
options["model"].created_by = current_user
end
def assign!(*, params:, model:, **)
model.title= params[:title]
end
def validate!(options, model:, **)
options["result.validate"] = ( model.created_by && model.title )
end
end
All three steps add data to the options
object. That data can be used in the following steps.
def validate!(options, model:, **)
options["result.validate"] = ( model.created_by && model.title )
end
It is a convention to use a "namespaced.key"
on the result object. This will help you structure and manage the data. It’s clever to namespace your data with something like my.
.
Some steps, such as Contract
or Policy
will add nested result objects under their own keys, like ["result.policy"]
. It’s a convention to add binary Result
objects to the “global” result under the ["result.XXX"]
key.
Result: API
After running the operation, the result object can be used for reading state.
result = Song::Create.({ title: "Roxanne" }, "current_user" => current_user)
result["model"] #=> #<Song title="Roxanne", "created_by"=<User ...>
result["result.validate"] #=> true
You can ask about the outcome of the operation via success?
and failure?
.
result.success? #=> true
result.failure? #=> falsee
Please note that the result object is also used to transport externally injected dependencies and class dependencies..
result["current_user"] #=> <User ...>
Use Result#inspect
to test a number of dependencies.
result.inspect("current_user", "model") #=> "<Result:true [#<User email=\"nick@tra... "
Result: Interpretation
The result object represents the interface between the operation’s inner happenings and the outer world, or, in other words, between implementation and user.
It’s up to you how you interpret all this available data. The binary state will help you, but arbitrary state can be transported. For a generic handling in HTTP context (Rails controllers or Rack routes), see → Endpoint
.
Dependencies
In an operation, there is only one way to manage dependencies and state: the options
object (sometimes also called skills hash or context object) is what gives access to class, runtime and injected runtime data.
State can be added on the class layer.
class Song::Create < Trailblazer::Operation
self["my.model.class"] = Song
# ...
end
Unsurprisingly, this is also readable on the class layer.
Song::Create["my.model.class"] #=> Song
This mechanism is used by all DSL methods such as contract
and by almost all step macros (e.g. Contract::Build
) to save and access class-wide data.
Class data is also readable at runtime in steps.
class Song::Create < Trailblazer::Operation
self["my.model.class"] = Song
step :model!
def model!(options, **)
options["my.model"] = # setting runtime data.
options["my.model.class"].new # reading class data at runtime.
end
end
In steps, you can set runtime data (e.g. my.model
).
After running the operation, this options
object turns into the result object.
result = Song::Create.({})
result["my.model.class"] #=> Song
result["my.model"] #=> #<Song title=nil>
Dependency Injection
Both class data as well as runtime data described above can be overridden using dependency injection.
result = Song::Create.({}, "my.model.class" => Hit)
result["my.model"] #=> #<Hit id=nil>
Note that injected dependencies need to be in the second argument to Operation::call
.
Policy::Pundit
or Contract::Validate
use the same mechanism, you can override hard-coded dependencies such as policies or contracts from the outside at runtime.
Be careful, though, with DI: It couples your operation to the caller and should be properly unit-tested or further encapsulated in e.g. Endpoint
.
Dependency Injection: Auto_inject
The operation supports Dry.RB’s auto_inject.
# this happens somewhere in your Dry system.
my_container = Dry::Container.new
my_container.register("repository.song", Song::Repository)
require "trailblazer/operation/auto_inject"
AutoInject = Trailblazer::Operation::AutoInject(my_container)
class Song::Create < Trailblazer::Operation
include AutoInject["repository.song"]
step :model!
def model!(options, params:, **)
options["model"] =
options["repository.song"].find_or_create( params[:id] )
end
end
Including the AutoInject
module will make sure that the specified dependencies are injected (using dependency injection) into the operation’s context at instantiation time.
Inheritance
To share code and pipe, use class inheritance.
Try to avoid inheritance and use composition instead.
You can inherit from any kind of operation.
class New < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: MyContract )
end
In this example, the New
class will have a pipe as follows.
0 =======================>>operation.new
1 ==========================&model.build
2 =======================>contract.build
In addition to Ruby’s normal class inheritance semantics, the operation will also copy the pipe. You may then add further steps to the subclass.
class Create < New
step Contract::Validate()
step Contract::Persist()
end
This results in the following pipe.
0 =======================>>operation.new
1 ==========================&model.build
2 =======================>contract.build
3 ==============&contract.default.params
4 ============&contract.default.validate
5 =========================&persist.save
Inheritance is great to eliminate redundancy. Pipes and step code can easily be shared amongst groups of operations.
Be weary, though, that you are tightly coupling flow and implementation to each other. Once again, try to use Trailblazer’s compositional semantics over inheritance.
Inheritance: Override
When using inheritance, use :override
to replace existing steps in subclasses.
Consider the following base operation.
module MyApp::Operation
class New < Trailblazer::Operation
extend Contract::DSL
contract do
property :title
end
step Model( nil, :new )
step Contract::Build()
end
end
Subclasses can now override predefined steps.
class Song::New < MyApp::Operation::New
step Model( Song, :new ), override: true
end
The pipe flow will remain the same.
Song::New["pipetree"].inspect(style: :row)
0 =======================>>operation.new
1 ==========================&model.build
2 =======================>contract.build
Refrain from using the :override
option if you want to add steps.
Options
When adding steps using step
, failure
and success
, you may name steps explicitly or specify the position.
Options: Name
A step macro will name itself.
class New < Trailblazer::Operation
step Model( Song, :new )
end
You can find out the name by inspecting the pipe.
0 =======================>>operation.new
1 ==========================&model.build
For every kind of step, whether it’s a macro or a custom step, use :name
to specify a name.
class New < Trailblazer::Operation
step Model( Song, :new ), name: "build.song.model"
step :validate_params!, name: "my.params.validate"
# ..
end
When inspecting the pipe, you will see your names.
0 =======================>>operation.new
1 =====================&build.song.model
2 ===================&my.params.validate
Assign manual names to steps when using macros multiple times, or when planning to alter the pipe in subclasses.
Options: Position
Whenever inserting a step, you may provide the position in the pipe using :before
or :after
.
class New < Trailblazer::Operation
step Model( Song, :new )
step :validate_params!, before: "model.build"
# ..
end
This will insert the custom step before the model builder.
0 =======================>>operation.new
1 =====================&validate_params!
2 ==========================&model.build
Naturally, :after
will insert the step after an existing one.
Options: Inheritance
The position options are extremely helpful with inheritance.
class Create < New
step :policy!, after: "operation.new"
end
It allows inserting new steps without repeating the existing pipe.
0 =======================>>operation.new
1 ==============================&policy!
2 =====================&validate_params!
3 ==========================&model.build
Options: Replace
Replace existing (or inherited) steps using :replace
.
class Update < New
step Model(Song, :find_by), replace: "model.build"
end
The existing step will be discarded with the newly provided one.
0 =======================>>operation.new
2 ==========================&model.build
Options: Delete
Step Macros
Trailblazer provides predefined steps to for all kinds of business logic.
- Contract implements contracts, validation and persisting verified data using the model layer.
-
Nested
,Wrap
andRescue
are step containers that help with transactional features for a group of steps per operation. - All
Policy
-related macros help with authentication and making sure users only execute what they’re supposed to. - The
Model
macro can create and find models based on input.
Macro API
Implementing your own macros helps to create reusable code.
It’s advised to put macro code into namespaces to not pollute the global namespace.
module Macro
def self.MyPolicy(allowed_role: "admin")
step = ->(input, options) { options["current_user"].type == allowed_role }
[ step, name: "my_policy.#{allowed_role}" ] # :before, :replace, etc. work, too.
end
end
The macro itself is a function. Per convention, the name is capitalized. You can specify any set of arguments (e.g. using kw args), and it returns a 2-element array with the actual step to be inserted into the pipe and default options.
Note that in the macro, the code design is up to you. You can delegate to other functions, objects, etc.
The macro step receives (input, options)
where input
is usually the operation instance and options
is the context object passed from step to step. It’s your macro’s job to read and write to options
. It is not advisable to interact with the operation instance.
Operations can then use your macro the way you’ve done it with our Trailblazer macros.
class Create < Trailblazer::Operation
step Macro::MyPolicy( allowed_role: "manager" )
# ..
end
When looking at the operation’s pipe, you can see how, in our example, the default options provide a convenient step name.
puts Create["pipetree"].inspect(style: :rows) #=>
0 ========================>operation.new
1 ====================>my_policy.manager
It is not advised to test macros in isolation. Note that it is possible by simply calling your macro in a test case. However, macros should be tested via an operation unit test to make sure the wiring is correct.
In future versions (TRB 2.0.2+) we will have public APIs for creating nested pipes, providing temporary arguments to steps and allowing injectable options.
Model
An operation can automatically find or create a model for you depending on the input, with the Model
macro.
class Create < Trailblazer::Operation
step Model( Song, :new )
# ..
end
After this step, there is a fresh model instance under options["model"]
that can be used in all following steps.
result = Create.({})
result["model"] #=> #<struct Song id=nil, title=nil>
Internally, Model
macro will simply invoke Song.new
to populate "model"
.
Model: Find_by
You can also find models using :find_by
. This is helpful for Update
or Delete
operations.
class Update < Trailblazer::Operation
step Model( Song, :find_by )
# ..
end
The Model
macro will invoke the following code for you.
options["model"] = Song.find_by( params[:id] )
This will assign ["model"]
for you by invoking find_by
.
result = Update.({ id: 1 })
result["model"] #=> #<struct Song id=1, title="Roxanne">
If Song.find_by
returns nil
, this will deviate to the left track, skipping the rest of the operation.
result = Update.({})
result["model"] #=> nil
result.success? #=> false
Note that you may also use :find
. This is not recommended, though, since it raises an exception, which is not the preferred way of flow control in Trailblazer.
Model: Arbitrary Finder
It’s possible to specify any finder method, which is helpful with ROMs such as Sequel.
class Show < Trailblazer::Operation
step Model( Song, :[] )
# ..
end
The provided method will be invoked and Trailblazer passes it the params[:id]
value.
Song[ params[:id] ]
Given your database gem provides that finder, it will result in a successful query.
result = Show.({ id: 1 })
result["model"] #=> #<struct Song id=1, title="Roxanne">
Nested
It is possible to nest operations, as in running an operation in another. This is the common practice for “presenting” operations and “altering” operations, such as Edit
and Update
.
class Edit < Trailblazer::Operation
extend Contract::DSL
contract do
property :title
end
step Model( Song, :find )
step Contract::Build()
end
Note how Edit
only defines the model builder (via :find
) and builds the contract.
Running Edit
will allow you to grab the model and contract, for presentation and rendering the form.
result = Edit.(id: 1)
result["model"] #=> #<Song id=1, title=\"Bristol\">
result["contract.default"] #=> #<Reform::Form ..>
This operation could now be leveraged in Update
.
class Update < Trailblazer::Operation
step Nested( Edit )
step Contract::Validate()
step Contract::Persist( method: :sync )
end
The Nested
macro helps you invoking an operation at any point in the pipe.
Nested: Data
The nested operation (Edit
) will, per default, only receive runtime data from the composing operation (Update
). Mutable data is not available to protect the nested operation from unsolicited input.
After running the nested Edit
operation its runtime data (e.g. "model"
) is available in the Update
operation.
result = Update.(id: 1, title: "Call It A Night")
result["model"] #=> #<Song id=1 , title=\"Call It A Night\">
result["contract.default"] #=> #<Reform::Form ..>
Should the nested operation fail, for instance because its model couldn’t be found, then the outer pipe will also jump to the left track.
Nested: Callable
If you need to pick the nested operation dynamically at runtime, use a callable object instead.
class Delete < Trailblazer::Operation
step Nested( MyBuilder )
# ..
end
The object’s call
method has the exact same interface as any other step.
class MyBuilder
extend Uber::Callable
def self.call(options, current_user:nil, **)
current_user.admin? ? Create::Admin : Create::NeedsModeration
end
end
Note that Nested
also works with :instance_method
and lambdas.
class Update < Trailblazer::Operation
step Nested( :build! )
def build!(options, current_user:nil, **)
current_user.admin? ? Create::Admin : Create::NeedsModeration
end
end
Nested: Input
Per default, only the runtime data will be passed into the nested operation. You can use :input
to change the data getting passed on.
The following operation multiplies two factors.
class Multiplier < Trailblazer::Operation
step ->(options, x:, y:, **) { options["product"] = x*y }
end
It is Nested
in another operation.
class MultiplyByPi < Trailblazer::Operation
step ->(options) { options["pi_constant"] = 3.14159 }
step Nested( Multiplier, input: ->(options, mutable_data:, runtime_data:, **) do
{ "y" => mutable_data["pi_constant"],
"x" => runtime_data["x"] }
end )
end
The examplary composing operation uses both runtime and mutable data. Its invocation could look as follows.
result = MultiplyByPi.({}, "x" => 9)
result["product"] #=> [28.27431]
The :input
option for Nested
allows to specify what options will go into the nested operation. It passes mutable_data
and runtime_data
to the option for your convenience.
While the runtime data normally gets passed on to nested operations automatically, mutual data won’t. :input
has to compile all data being passed on to the nested operation and return that hash.
Use a Callable
to share mapping code.
class MyInput
extend Uber::Callable
def self.call(options, mutable_data:, runtime_data:, **)
{
"y" => mutable_data["pi_constant"],
"x" => runtime_data["x"]
}
end
end
It will improve readability.
class MultiplyByPi < Trailblazer::Operation
step ->(options) { options["pi_constant"] = 3.14159 }
step Nested( Multiplier, input: MyInput )
end
Nested: Output
After running a nested operation, its mutable data gets copied into the options
of the composing operation.
Use :output
to change that, should you need only specific values.
class Update < Trailblazer::Operation
step Nested( Edit, output: ->(options, mutable_data:, **) do
{
"contract.my" => mutable_data["contract.default"],
"model" => mutable_data["model"]
}
end )
step Contract::Validate( name: "my" )
step Contract::Persist( method: :sync, name: "my" )
end
This works with lambdas, :method
and Callable
.
Wrap
Steps can be wrapped by an embracing step. This is necessary when defining a set of steps to be contained in a database transaction or a database lock.
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
end
step Wrap ->(*, &block) { Sequel.transaction do block.call end } {
step Model( Song, :new )
step Contract::Build( constant: MyContract )
step Contract::Validate( )
step Contract::Persist( method: :sync )
}
failure :error! # handle all kinds of errors.
step :notify!
def error!(options)
# handle errors after the wrap
end
def notify!(options)
MyNotifier.mail
end
end
The Wrap
macro helps you to define the wrapping code (such as a Sequel.transaction
call) and allows you to define the wrapped steps. (Because of the precedence works in Ruby, you need to use {...}
instead of do...end
.)
class Create < Trailblazer::Operation
# ...
step Wrap ->(*, &block) { Sequel.transaction do block.call end } {
step Model( Song, :new )
# ...
}
failure :error! # handle all kinds of errors.
# ...
end
As always, you can have steps before and after Wrap
in the pipe.
The proc passed to Wrap
will be called when the pipe is executed, and receives block
. block.call
will execute the nested pipe.
You may have any amount of Wrap
nesting.
Wrap: Return Value
All nested steps will simply be executed as if they were on the “top-level” pipe, but within the wrapper code. Steps may deviate to the left track, and so on.
However, the last signal of the wrapped pipe is not simply passed on to the “outer” pipe. The return value of the actual Wrap
block is crucial: If it returns falsey, the pipe will deviate to left after Wrap
.
step Wrap ->(*, &block) { Sequel.transaction do block.call end; false } {
In the above example, regardless of Sequel.transaction
’s return value, the outer pipe will deviate to the left track as the Wrap
’s return value is always false
.
Wrap: Callable
For reusable wrappers, you can also use a Callable
object.
class MyTransaction
extend Uber::Callable
def self.call(options, *)
Sequel.transaction { yield } # yield runs the nested pipe.
# return value decides about left or right track!
end
end
This can then be passed to Wrap
, making the pipe extremely readable.
class Create < Trailblazer::Operation
# ...
step Wrap( MyTransaction ) {
step Model( Song, :new )
# ...
}
failure :error! # handle all kinds of errors.
# ...
end
Rescue
While you can write your own begin/rescue/end
mechanics using Wrap
, Trailblazer offers you the Rescue
macro to catch and handle exceptions that might occur while running the pipe.
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
end
step Rescue {
step Model(Song, :find)
step Contract::Build( constant: MyContract )
}
step Contract::Validate()
step Contract::Persist( method: :sync )
end
Any exception raised during a step in the Rescue
block will stop the nested pipe from being executed, and continue after the block on the left track.
You can specify what exceptions to catch and an optional handler that is called when an exception is encountered.
class Create < Trailblazer::Operation
step Rescue( RecordNotFound, KeyError, handler: :rollback! ) {
step Model( Song, :find )
step Contract::Build( constant: MyContract )
}
step Contract::Validate()
step Contract::Persist( method: :sync )
def rollback!(exception, options)
options["x"] = exception.class
end
end
Alternatively, you can use a Callable
object for :handler
.
Full Example
The Nested
, Wrap
and Rescue
macros can also be nested, allowing an easily extendable business workflow with error handling along the way.
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
end
step Rescue( RecordNotFound, handler: :rollback! ) {
step Wrap ->(*, &block) { Sequel.transaction do block.call end } {
step Model( Song, :find )
step ->(options) { options["model"].lock! } # lock the model.
step Contract::Build( constant: MyContract )
step Contract::Validate( )
step Contract::Persist( method: :sync )
}
}
failure :error! # handle all kinds of errors.
def rollback!(exception, options)
# ...
end
def error!(options)
# ...
end
end