{{ page.title }}
- Last updated 05 May 2017
- representable v3.0
Operation
- Last updated 08 Dec 20
Overview
Operations have been a central element since Trailblazer 1.0. An operation models a top-level function of your application, such as creating a user or archiving a blog post. It’s the outer-most object that is directly invoked by the controller, embraces all business-specific logic and hides it from the latter.
Operations are often confused as god objects that do “everything”. However, operations are nothing but orchestrators. Where to implement the actual code and when to call it is up to the developer.
Since Trailblazer 2.1, operations are reduced to being a very thin, public API around an Activity
with some pre-defined configuration such as the FastTrack
-railway.
Deeply nested business logic is implemented using activities. For background-compatibility, you may still use an operation for the upper-most logic, but internally, it boils down to being an Activity::FastTrack
.
- Two public
Operation.call
signatures: “public call” and circuit interface. *
Invocation
An operation has two invocation styles. This is the only difference to an Activity
.
Until TRB 2.1, the public call was the only way to invoke an operation. Its signature is simpler, but less powerful.
result = Memo::Create.(params: {text: "Enjoy an IPA"})
puts result.success? #=> true
model = result[:model]
puts model.text #=> "Enjoy an IPA"
The public call will return a result object that exposes the binary state (success?
or failure?
). All variables written to the context are accessable via the #[]
reader.
Since operations are just normal activities under the hood, they also expose the [circuit interface]. This allows using all advanced features such as [taskWrap], [tracing] or nesting operations with the generic activity mechanics.
ctx = {params: {text: "Enjoy an IPA"}}
signal, (ctx, _) = Memo::Create.([ctx, {}], {})
puts signal #=> #<Trailblazer::Activity::Railway::End::Success semantic=:success>
Wiring
An operation is simply an Activity::FastTrack
subclass and all [DSL implications are identical].
class Create < Trailblazer::Operation
step :validate, fast_track: true
fail :log_error
step :create
# ...
end
An operation always allows you the fast-track outputs and wiring.
For DSL options, refer to Fast Track.
Result
An operation invoked with public call will return an Operation::Result
object for your convenience. It’s nothing but a container exposing the binary state (or outcome) plus the ctx
object that was passed around in the circuit.
class Create < Trailblazer::Operation
step :validate, fast_track: true
fail :log_error
step :create
def create(ctx, **)
ctx[:model] = Memo.new
end
# ...
# ...
The result exposes state and the context you wrote to.
result.success? #=> true
result[:model] #=> #<Memo ..>
The operation ending on a “failure” end (End.failure
, End.fail_fast
) will result in result.failure?
being true. All other outcomes will be interpreted as success.
Please note that the result object is not compatible with the circuit interface and only here for backward-compatibility, when invoking operations manually.
In compositions or workflows, operations will always be called using the circuit interface.
Macros
Trailblazer ships with a handful of step functions called macros. Those are implemented in trailblazer-macro and trailblazer-macro-contract.
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.(params: {})
result[:model] #=> #<struct Song id=nil, title=nil>
Internally, Model
macro will simply invoke Song.new
to populate :model
.
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( id: params[:id] )
This will assign [:model]
for you by invoking find_by
.
result = Update.(params: { id: 1 })
result[:model] #=> #<struct Song id=1, title="nil">
If Song.find_by
returns nil
, this will deviate to the left track, skipping the rest of the operation.
result = Update.(params: {})
result[:model] #=> nil
result.success? #=> false
It is also possible to find_by
with attribute other than id
. For example,
class UpdateWithFindByKey < Trailblazer::Operation
step Model( Song, :find_by, :title )
# ..
end
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.
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.(params: { id: 1 })
result[:model] #=> #<struct Song id=1, title="Roxanne">
With the help of endpoint, it is possible to connect failure of this step to render 404 response directly.
As a result, you don’t need to conditionally check if ctx[:model]
is present or not.
To emit End.not_found
signal, you need to pass not_found_terminus
kwarg.
class UpdateFailureWithModelNotFound < Trailblazer::Operation
step Model( Song, :find_by, not_found_terminus: true )
# ..
end
Internally, it is just telling Output(:failure)
to emit End(:not_found)
result = UpdateFailureWithModelNotFound.(params: {title: nil})
result[:model] #=> nil
result.success? #=> false
result.event #=> #<Trailblazer::Activity::End semantic=:not_found>
Nested
The Nested
macro works identical to Subprocess
when you pass an activity.
class Create < Trailblazer::Operation
step :create
step Nested(Validate)
step :save
# ...
end
It will print a deprecation and use Subprocess
internally, automatically wiring the nested’s outputs (Validate
) to the known outer tracks.
Anyhow, Nested
allows for dynamically deciding the nested activity at run-time using an :instance_method
, or any callable object.
class Create < Trailblazer::Operation
step :create
step Nested(:compute_nested)
step :save
def compute_nested(ctx, params:, **)
params.is_a?(Hash) ? Validate : JsonValidate
end
# ...
end
The dynamic nested decider method or callable has a task interface and must return the activity to nest. Nested
will automatically connect the failure
and the success
end, only.
We’re currently investigating the best strategy for Nested’s auto-wiring. Please hit us up if you need additions or have groundbreaking ideas.
You may use the wiring DSL with Output
and friends to connect outputs from the nested activity.
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 Memo::Create < Trailblazer::Operation
step :find_model
step Wrap( MyTransaction ) {
step :update
step :rehash
}
step :notify
fail :log_error
# ...
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 HandleUnsafeProcess
def self.call((_ctx, _flow_options), *, &block)
signal, (ctx, flow_options) = yield
evaluated_signal = if signal.to_h[:semantic] == :success
Trailblazer::Operation::Railway.pass_fast!
else
Trailblazer::Operation::Railway.fail!
end
[ evaluated_signal, [ctx, flow_options] ]
end
end
As always, you can have steps before and after Wrap
in the pipe.
The proc passed to Wrap
will be called when the step is executed, and receives block
. block.call
will execute the nested pipe.
You may have any amount of Wrap
nesting.
For reusable wrappers, you can also use a Callable
object.
class MyTransaction
def self.call((ctx, flow_options), *, &block)
Sequel.transaction { yield } # calls the wrapped steps
rescue
[ Trailblazer::Operation::Railway.fail!, [ctx, flow_options] ]
end
end
This can then be passed to Wrap
, making the flow extremely readable.
All nested steps will simply be executed as if they were on the “top-level” track, 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
.
TODO: document how you can wire
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 Memo::Create < Trailblazer::Operation
step :find_model
step Rescue( RuntimeError, handler: MyHandler ) {
step :update
step :rehash
}
step :notify
fail :log_error
# ...
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 MyHandler
def self.call(exception, (ctx), *)
ctx[:exception_class] = exception.class
end
end
Alternatively, you can use a Callable
object for :handler
.
The Nested
, Wrap
and Rescue
macros can also be nested, allowing an easily extendable business workflow with error handling along the way.
TODO: add example
Policy
The Policy
macros Policy::Pundit
, and Policy::Guard
help to implement permission decider steps.
Pundit
The Policy::Pundit
module allows using Pundit-compatible policy classes in an operation.
A Pundit policy has various rule methods and a special constructor that receives the current user and the current model.
class MyPolicy
def initialize(user, model)
@user, @model = user, model
end
def create?
@user == Module && @model.id.nil?
end
def new?
@user == Class
end
end
In pundit policies, it is a convention to have access to those objects at runtime and build rules on top of those.
You can plug this policy into your pipe at any point. However, this must be inserted after the "model"
skill is available.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create? )
# ...
end
Note that you don’t have to create the model via the Model
macro - you can use any logic you want. The Pundit
macro will grab the model from ["model"]
, though.
This policy will only pass when the operation is invoked as follows.
Create.(current_user: User.find(1))
Any other call will cause a policy breach and stop the pipe from executing after the Policy::Pundit
step.
Add your polices using the Policy::Pundit
macro. It accepts the policy class name, and the rule method to call.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create? )
# ...
end
The step will create the policy instance automatically for you and passes the "model"
and the "current_user"
skill into the policies constructor. Just make sure those dependencies are available before the step is executed.
If the policy returns falsey
, it deviates to the left track.
After running the Pundit
step, its result is readable from the Result
object.
result = Create.(params: {}, current_user: Module)
result[:"result.policy.default"].success? #=> true
result[:"result.policy.default"][:policy] #=> #<MyPolicy ...>
Note that the actual policy instance is available via ["result.policy.#{name}"]["policy"]
to be reinvoked with other rules (e.g. in the view layer).
You can add any number of Pundit policies to your pipe. Make sure to use name:
to name them, though.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create?, name: "after_model" )
# ...
end
The result will be stored in "result.policy.#{name}"
result = Create.(params: {}, current_user: Module)
result[:"result.policy.after_model"].success? #=> true
Override a configured policy using dependency injection.
Create.(params: {},
current_user: Module,
:"policy.default.eval" => Trailblazer::Operation::Policy::Pundit.build(AnotherPolicy, :create?)
)
You can inject it using "policy.#{name}.eval"
. It can be any object responding to call
.
Guard
A guard is a step that helps you evaluating a condition and writing the result. If the condition was evaluated as falsey
, the pipe won’t be further processed and a policy breach is reported in Result["result.policy.default"]
.
class Create < Trailblazer::Operation
step Policy::Guard(->(options, pass:, **) { pass })
step :process
def process(options, **)
options[:x] = true
end
end
The only way to make the above operation invoke the second step :process
is as follows.
result = Create.({ pass: true })
result["x"] #=> true
Any other input will result in an abortion of the pipe after the guard.
result = Create.()
result["x"] #=> nil
result["result.policy.default"].success? #=> false
Learn more about → dependency injection to pass params and current user into the operation. TODO: fix link
The Policy::Guard
macro helps you inserting your guard logic. If not defined, it will be evaluated where you insert it.
step :process
def process(options, **)
options[:x] = true
end
The options
object is passed into the guard and allows you to read and inspect data like params
or current_user
. Please use kw args.
As always, the guard can also be a Callable
-marked object.
class MyGuard
def call(options, pass:, **)
pass
end
end
Insert the object instance via the Policy::Guard
macro.
class Create < Trailblazer::Operation
step Policy::Guard( MyGuard.new )
step :process
# ...
end
As always, you may also use an instance method to implement a guard.
class Create < Trailblazer::Operation
step Policy::Guard( :pass? )
def pass?(options, pass:, **)
pass
end
step :process
# ...
end
The guard name defaults to default
and can be set via name:
. This allows having multiple guards.
class Create < Trailblazer::Operation
step Policy::Guard( ->(options, current_user:, **) { current_user }, name: :user )
# ...
end
The result will sit in result.policy.#{name}
.
result = Create.(:current_user => true)
result[:"result.policy.user"].success? #=> true
Instead of using the configured guard, you can inject any callable object that returns a Result
object. Do so by overriding the policy.#{name}.eval
path when calling the operation.
Create.(
:current_user => Module,
:"policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options, **) { false })
)
An easy way to let Trailblazer build a compatible object for you is using Guard.build
.
This is helpful to override a certain policy for testing, or to invoke it with special rights, e.g. for an admin.
You may specify a position.
class Create < Trailblazer::Operation
step :model!
step Policy::Guard( :authorize! ),
before: :model!
end
Resulting in the guard inserted before model!
, even though it was added at a later point.
Trailblazer::Developer.railway(Create, style: :rows) #=>
# 0 ========================>operation.new
# 1 ==================>policy.default.eval
# 2 ===============================>model!
This is helpful if you maintain modules for operations with generic steps.
Contract
A contract is an abstraction to handle validation of arbitrary data or object state. It is a fully self-contained object that is orchestrated by the operation.
The actual validation can be implemented using Reform with ActiveModel::Validation
or dry-validation, or a Dry::Schema
directly without Reform.
The Contract
macros helps you defining contracts and assists with instantiating and validating data with those contracts at runtime.
overview: reform
Most contracts are Reform objects that you can define and validate in the operation. Reform is a fantastic tool for deserializing and validating deeply nested hashes, and then, when valid, writing those to the database using your persistence layer such as ActiveRecord.
# app/concepts/song/contract/create.rb
module Song::Contract
class Create < Reform::Form
property :title
property :length
validates :title, length: 2..33
validates :length, numericality: true
end
end
The contract then gets hooked into the operation.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate()
step Contract::Persist()
end
As you can see, using contracts consists of five steps.
- Define the contract class (or multiple of them) for the operation.
- Plug the contract creation into the operation’s pipe using
Contract::Build
. - Run the contract’s validation for the params using
Contract::Validate
. - If successful, write the sane data to the model(s). This will usually happen in the
Contract::Persist
macro. - After the operation has been run, interpret the result. For instance, a controller calling an operation will render a erroring form for invalid input.
You don’t have to use any of the TRB macros to deal with contracts, and do everything yourself. They are an abstraction that will save code and bugs, and introduce strong conventions. However, feel free to use your own code.
Here’s what the result would look like after running the Create
operation with invalid data.
result = Song::Create.(params: { title: "A" })
result.success? #=> false
result[:"contract.default"].errors.messages
#=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}
Definition
Trailblazer offers a few different ways to define contract classes and use them in an operation.
The preferred way of defining contracts is to use a separate file and class, such as the example below.
# app/concepts/song/contract/create.rb
module Song::Contract
class Create < Reform::Form
property :title
property :length
validates :title, length: 2..33
validates :length, numericality: true
end
end
This is called explicit contract.
The contract file could be located just anywhere, but it’s clever to follow the Trailblazer conventions.
Using the contract happens via Contract::Build
, and the :constant
option.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate()
step Contract::Persist()
end
Since both operations and contracts grow during development, the completely encapsulated approach of the explicit contract is what we recommend.
Contracts can also be defined in the operation itself.
# app/concepts/song/create.rb
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
property :length
validates :title, presence: true
validates :length, numericality: true
end
step Model( Song, :new )
step Contract::Build(constant: MyContract)
step Contract::Validate()
step Contract::Persist( method: :sync )
end
Defining the contract happens via the contract
block. This is called an inline contract. Note that you need to extend the class with the Contract::DSL
module. You don’t have to specify anything in the Build
macro.
While this is nice for a quick example, this usually ends up quite convoluted and we advise you to use the explicit style.
Build
The Contract::Build
macro helps you to instantiate the contract.
It is both helpful for a complete workflow, or to create the contract, only, without validating it, e.g. when presenting the form.
class Song::New < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
end
This macro will grab the model from options["model"]
and pass it into the contract’s constructor. The contract is then saved in options["contract.default"]
.
result = Song::New.(params: {})
result["model"] #=> #<struct Song title=nil, length=nil>
result["contract.default"]
#=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>
The Build
macro accepts the :name
option to change the name from default
.
Validate
The Contract::Validate
macro is responsible for validating the incoming params against its contract. That means you have to use Contract::Build
beforehand, or create the contract yourself. The macro will then grab the params and throw then into the contract’s validate
(or call
) method.
class Song::ValidateOnly < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate()
end
Depending on the outcome of the validation, it either stays on the right track, or deviates to left, skipping the remaining steps.
result = Song::ValidateOnly.(params: {}) # empty params
result.success? #=> false
Note that Validate
really only validates the contract, nothing is written to the model, yet. You need to push data to the model manually, e.g. with Contract::Persist
.
result = Song::ValidateOnly.(params: { title: "Rising Force", length: 13 })
result.success? #=> true
result[:model] #=> #<struct Song title=nil, length=nil>
result[:"contract.default"].title #=> "Rising Force"
Validate
will use options["params"]
as the input. You can change the nesting with the :key
option.
Internally, this macro will simply call Form#validate
on the Reform object.
Note that Reform comes with sophisticated deserialization semantics for nested forms, it might be worth reading a bit about Reform to fully understand what you can do in the Validate
step.
Key
Per default, Contract::Validate
will use options["params"]
as the data to be validated. Use the key:
option if you want to validate a nested hash from the original params structure.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate( key: "song" )
step Contract::Persist( )
end
This automatically extracts the nested "song"
hash.
result = Song::Create.(params: { "song" => { title: "Rising Force", length: 13 } })
result.success? #=> true
If that key isn’t present in the params hash, the operation fails before the actual validation.
result = Song::Create.(params: { title: "Rising Force", length: 13 })
result.success? #=> false
Note that string vs. symbol do matter here since the operation will simply do a hash lookup using the key you provided.
Invalid Termini
If the Contract::Validate()
deviates on a failure track, it is possible to emit a new signal suggesting contract failure.
This becomes really handy when used along with the endpoint.
It avoids any conditional checks and can be wired to render 422 response without accessing the ctx
.
In order to add this new termini in your operation’s terminuses, you need to pass invalid_data_terminus
kwarg.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate( key: :song, invalid_data_terminus: true )
step Contract::Persist( )
end
Based on the given name to this macro (default is ofcourse, default
), it will assign End
semantic as "contract.#{name}.invalid"
.
result = Song::Create.(params: { title: "Rising Force", length: 13 })
result.success? #=> false
result.event #=> #<Trailblazer::Activity::End semantic=:"contract.default.invalid">
Persist
To push validated data from the contract to the model(s), use Persist
. Like Validate
, this requires a contract to be set up beforehand.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( constant: Song::Contract::Create )
step Contract::Validate()
step Contract::Persist()
end
After the step, the contract’s attribute values are written to the model, and the contract will call save
on the model.
result = Song::Create.(params: { title: "Rising Force", length: 13 })
result.success? #=> true
result["model"] #=> #<Song title="Rising Force", length=13>
You can also configure the Persist
step to call sync
instead of Reform’s save
.
step Persist( method: :sync )
This will only write the contract’s data to the model without calling save
on it.
Name
Explicit naming for the contract is possible, too.
class Song::Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build( name: "form", constant: Song::Contract::Create )
step Contract::Validate( name: "form" )
step Contract::Persist( name: "form" )
end
You have to use the name:
option to tell each step what contract to use. The contract and its result will now use your name instead of default
.
result = Song::Create.(params: { title: "A" })
result[:"contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...
Use this if your operation has multiple contracts.
Dry-Schema
It is possible to use a Dry::Schema directly as a contract. This is great for stateless, formal validations, e.g. to make sure the params have the right format.
require "reform/form/dry"
class Create < Trailblazer::Operation
# contract to verify params formally.
class MyContract < Reform::Form
feature Reform::Form::Dry
property :id
property :title
validation name: :default do
required(:id).filled
end
validation name: :extra, if: :default do
required(:title).filled(min_size?: 2)
end
end
step Model( Song, :new ) # create the op's main model.
step Contract::Build( constant: MyContract ) # create the Reform contract.
step Contract::Validate() # validate the Reform contract.
step Contract::Persist( method: :sync) # persist the contract's data via the model.
end
Schema validations don’t need a model and hence you don’t have to instantiate them.
Dry’s schemas can even be executed before the operation gets instantiated, if you want that. This is called a guard schema and great for a quick formal check. If that fails, the operation won’t be instantiated which will save time massively.
TODO: DOCUMENT
Use schemas for formal, linear validations. Use Reform forms when there’s a more complex deserialization with nesting and object state happening.
As always, you can also use an explicit schema.
TODO: document
Manual Extraction
You can plug your own complex logic to extract params for validation into the pipe.
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
end
def type
"evergreen" # this is how you could do polymorphic lookups.
end
step Model( Song, :new )
step Contract::Build(constant: MyContract)
step :extract_params!
step Contract::Validate( skip_extract: true )
step Contract::Persist( method: :sync )
def extract_params!(options, **)
options[:"contract.default.params"] = options[:params][type]
end
end
Note that you have to set the self["params.validate"]
field in your own step, and - obviously - this has to happen before the actual validation.
Keep in mind that &
will deviate to the left track if your extract_params!
logic returns falsey.
Dependency Injection
In fact, the operation doesn’t need any reference to a contract class at all.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Contract::Build()
step Contract::Validate()
step Contract::Persist( method: :sync )
end
The contract can be injected when calling the operation.
A prerequisite for that is that the contract class is defined.
class MyContract < Reform::Form
property :title
validates :title, length: 2..33
end
When calling, you now have to provide the default contract class as a dependency.
Create.(
params: { title: "Anthony's Song" },
:"contract.default.class" => MyContract
)
This will work with any name if you follow the naming conventions.
Manual Build
To manually build the contract instance, e.g. to inject the current user, use builder:
.
class Create < Trailblazer::Operation
class MyContract < Reform::Form
property :title
property :current_user, virtual: true
validate :current_user?
validates :title, presence: true
def current_user?
return true if defined?(current_user)
false
end
end
step Model( Song, :new )
step Contract::Build( constant: MyContract, builder: :default_contract! )
step Contract::Validate()
step Contract::Persist( method: :sync )
def default_contract!(options, constant:, model:, **)
constant.new(model, current_user: options [:current_user])
end
end
Note how the contract’s class and the appropriate model are offered as kw arguments. You’re free to ignore these options and use your own assets.
As always, you may also use a proc.
Result Object
The operation will store the validation result for every contract in its own result object.
The path is result.contract.#{name}
.
result = Create.(params: { length: "A" })
result[:"result.contract.default"].success? #=> false
result[:"result.contract.default"].errors #=> Errors object
result[:"result.contract.default"].errors.messages #=> {:length=>["is not a number"]}
Each result object responds to success?
, failure?
, and errors
, which is an Errors
object. TODO: design/document Errors. WE ARE CURRENTLY WORKING ON A UNIFIED API FOR ERRORS (FOR DRY AND REFORM).
Options
Class Dependencies
If you want to configure values or dependencies on the operation class level, use the ClassDependencies
module.
The usage of this feature is not recommended. Use a dry-container instead.
You may use the self[]=
setter to add directives to the operation class.
These variables will be available within every step of the operation and after.
class Create < Trailblazer::Operation
extend ClassDependencies
# Configure some dependency on class level
self[:validator] = AlwaysTrue
step :validate
step Subprocess(Insert)
def validate(ctx, validator:, params:, **)
validator.(params)
end
end
Starting with the invocation of the Create
operation, the validator
variable is injected into the ctx
and passed on.
def validate(ctx, validator:, params:, **)
validator.(params)
end
The variable is readable from ctx
even when the operation finishes - so be careful in nested setups.
signal, (ctx, _) = Create.([ctx, {}])
puts ctx[:validator] #=> AlwaysTrue
Note that variables from within a nested operation are readable in the outer operation after the nested one has been invoked.
Signals
A signal is the object that is returned from a task. It can be any kind of object, but per convention, we derive signals from Trailblazer::Activity::Signal
. When using the wiring API with step
and friends, your tasks will automatically get wrapped so the returned boolean gets translated into a signal.
You can bypass this by returning a signal directly.
{{ “signal-validate” | tsnippet }} |
Historically, the signal name for taking the success track is Right
whereas the signal for the error track is Left
. Instead of using the signal constants directly (which some users, for whatever reason, prefer), you may use signal helpers. The following snippet is identical to the one above.
{{ “signalhelper-validate” | tsnippet }} |
Available signal helpers per default are Railway.pass!
, Railway.fail!
, Railway.pass_fast!
and Railway.fail_fast!
.
{% callout %}
Note that those signals must have outputs that are connected to the next task, otherwise you will get a IllegalOutputSignalError
exception. The PRO editor or tracing can help understanding.
Also, keep in mind that the more signals you use, the harder it will be to understand. This is why the operation enforces the :fast_track
option when you want to use pass_fast!
and fail_fast!
- so both the developer reading your operation and the framework itself know about the implications upfront.
{% endcallout %}