{{ page.title }}
- Last updated 05 May 2017
- representable v3.0
Trailblazer
- Last updated 29 Nov 21
About
The Trailblazer project started off with a single gem called trailblazer
. Currently, the framework consists of around 40 gems. The main gem trailblazer
is an umbrella gem with the sole reason to pull all default dependencies for you. Feel free to pick and choose what you need for your applications.
Are you new to TRB? Then it’s probably best to start on our LEARN page and find out more about this framework!
Overview
The following list of gems is an overview of the most important core components in Trailblazer.
Gem | Summary | Dependencies | Source | ||||
---|---|---|---|---|---|---|---|
CORE | |||||||
|
Glue code for conveniently using TRB in a Rails project. Adds Controller#run for invoking operations and Controller#cell to render cells.
|
|
|||||
|
Callable run-time circuits that control the execution flow of your tasks. |
|
|||||
|
DSL that provides Path , Railway and FastTrack activities.
|
|
|||||
|
Context implementation. |
|
|||||
|
Callables implementation. |
|
|||||
HELPER | |||||||
|
Implement the entire controller flow, from authentication until rendering, via activities. |
|
|||||
|
Thin API around FastTrack exposing the "old" call -API named Operation .
|
|
|||||
|
Provides Nested() , Model() and other macros for everyday usage.
|
|
|||||
|
Provides the Contract::Validate() macro and friends.
|
reform, dry-validation |
|
||||
TOOLS | |||||||
|
Activity visualization, tracing, debugging, PRO Editor communication. |
|
|||||
|
Minitest assertions and tools for fast TRB testing. |
|
|||||
|
Rspec testing extensions. |
|
|||||
PRO | |||||||
|
Long-running activities with process engine, events and collaboration. | ||||||
|
DSL for creating activities from PRO Editor exports. |
Along with the core gems goes a rich list of eco-system gems.
Gem | Summary | Dependencies | Source | ||||
---|---|---|---|---|---|---|---|
|
Form objects. |
|
|||||
|
Generic view components. |
|
|||||
|
Maps representation documents from and to Ruby objects. |
|
|||||
|
Decorators on top of your ORM layer. |
|
|||||
|
Parse and render REST API documents using representers. |
|
Rails
Trailblazer runs with any Ruby web framework. However, if you’re using Rails, you’re lucky since we provide convenient glue code in the trailblazer-rails
gem.
gem "trailblazer-rails"
This will automatically pull trailblazer
and trailblazer-loader
.
The Trailblazer::Rails::Railtie
railtie will activate all necessary convenience methods for you. You don’t have to do anything manually here.
Use trailblazer-rails
>= 2.0 for TRB 2.1 and above.
Controller
In order to run an operation in a controller you may simply invoke it manually.
class SongsController < ApplicationController
def create
ctx = Song::Operation::Create.(params: params)
@form = ctx[:contract]
render :new
end
end
Run
However, trailblazer-rails
gives you #run
for this to simplify the task.
class SongsController < ApplicationController
def create
run Song::Operation::Create
render :new
end
end
Without any configuration, #run
passes the controller’s params
hash into the operation call as in the example above.
It automatically assigns @model
and, if available, @form
for you.
The ctx result object is returned.
def create
ctx = run Song::Operation::Create
ctx[:model] #=> #<Song title=...>
render :new
end
The ctx object is also assigned to @_result
.
To handle success and failure cases, run accepts an optional block.
class SongsController < ApplicationController
def create
run Song::Operation::Create do |ctx|
return redirect_to song_path(ctx[:model].id)
end
render :new
end
end
The block is only run for success?
. The block argument is the operation’s result ctx.
In general, it’s a good idea to inject runtime dependencies such as :current_user
into the operation call.
Song::Operation::Create.(params: params, current_user: current_user)
Override #_run_options
to do that automatically for all run
calls in a controller.
class ApplicationController < ActionController::Base
private
def _run_options(options)
options.merge(
current_user: current_user
)
end
end
Instead of configuring variables on the controller-level, you can also fine-tune variables when invoking #run
.
class SongsController < ApplicationController
def create
run Song::Operation::Create, current_user: current_user, cookie: session do |ctx|
return redirect_to song_path(ctx[:model].id)
end
render :new
end
end
This will add :current_user
and :cookie
to the ctx passed into the Song::Operation::Create
operation.
Cells
The trailblazer-rails
gem makes it very simple to use the Cells gem along with TRB. It overrides ActionController#render
and allows to render a cell.
class SongsController < ApplicationController
def create
run Song::New # optional
render cell(Song::Cell::New, @model)
end
end
You simply invoke #cell
the way you did it before, and pass it to render
. If the first argument to #render
is not a cell, the original Rails render
version will be used, allowing you to use serializers, partials or whatever else you need.
Per default, #render
will add layout: true
to render the controller’s ActionView layout.
The controller’s layout can be disabled using layout: false
. These are pure Rails mechanics.
render cell(Song::Cell::New, @model), layout: false
In fact, any option passed to render
will be passed through to the controller’s #render
.
As you’re using #cell
from the Cells gem you may pass any option the cell understands.
render cell(Song::Cell::New, model, layout: Song::Cell::DarkLayout)
For instance, you can instruct the cell to use a separate layout cell. Note that this option is passed to #cell
, not to #render
.
You can set controller-wide options passed to every #cell
call by overriding the #options_for_cell
method.
class SongsController < ApplicationController
private def options_for_cell(model, options)
{
layout: Song::Cell::DarkLayout # used for all #cell calls.
}
end
end
This is extremely helpful for providing controller-wide options like :layout
, allowing you to skip this specific option in all actions.
class SongsController < ApplicationController
def create
# ..
render cell(Song::Cell::New, model) # no need to pass {layout: Song::Cell::DarkLayout}
end
This feature was added in trailblazer-rails
version 2.2.0.
Loader
The trailblazer-loader
gem implements a very simple way to load all files in your concepts
directory in a heuristically meaningful order. It can be used in any environment.
The trailblazer-loader
gem comes pre-bundled with trailblazer-rails
for historical reasons: in the early days of Trailblazer, the conventional file name concepts/product/operation/create.rb
didn’t match the short operation name, such as Product::Create
.
The trailblazer-loader
gem’s duty was to load all concept files without using Rails’ autoloader, overcoming the latter’s conventions.
Over the years, and with the emerge of controller helpers or our workflow engine calling operations for you, the class name of an operation more and becomes a thing not to worry about.
Many projects use Trailblazer along with the Rails naming convention now. This means you can disable the loader gem, and benefit from Rails auto-magic behavior such as faster loading in the “correct” order, reloading and all the flaws that come with this non-deterministic behavior.
As a first step, add Operation
to your operation’s class name, matching the Rails naming convention.
# app/concepts/product/operation/create.rb
module Product::Operation
class Create < Trailblazer::Operation
# ...
end
end
It’s a Trailblazer convention to put [ConceptName]::Operation
in one line: it will force Rails to load the concept name constant, so you don’t have to reopen the class yourself.
This will result in a class name Product::Operation::Create
.
Next, disable the loader gem, in config/initializers/trailblazer.rb
.
# config/initializers/trailblazer.rb
YourApp::Application.config.trailblazer.enable_loader = false
Trailblazer files will now be loaded by Rails - you need to follow the Rails autoloading file naming from here on, and things should run smoothly. A nice side-effect here is that in bigger projects (with hundreds of operations), the start-up time in development accelerates significantly.
The infamous warning: toplevel constant Cell referenced by Notification::Cell
warning is a bug in Ruby. You should upgrade to Ruby >= 2.5.
2.1 Migration
Call API
In versions before 2.1, the automatic merging of the params part and the additional options was confusing many new users and an unnecessary step.
# old style
result = Memo::Create.( params, "current_user" => current_user )
The first argument (params) was merged into the second argument using the key “params”. You now pass one hash to call and hence do the merging yourself.
# new style
result = Memo::Create.( params: params, current_user: current_user )
The new call API is much more consistent and takes away another thing we kept explaining to new users - an indicator for a flawed API.
For a soft deprecation, do this in an initializer:
require "trailblazer/deprecation/call"
You will get a bunch of warnings, so fix all Operation.()
calls and remove the require again. This is provied by trailblazer-future gem.
In case your steps expose a signature as follows, you’re safe.
class Memo::Create < Trailblazer::Operation
step :create_model
def create_model(options, params:, **)
# ..
end
end
By convention, we renamed option
to ctx
, but it is completely up to you to adopt this.
Nevertheless, the “old style” signatures won’t work anymore.
class Memo::Create < Trailblazer::Operation
def create_model(options)
# ..
end
# or
def create_model(model:, **)
# ..
end
end
Neither a single options
nor keyword-arguments-only are gonna fly as the new step signature is more restrictive and always requires you to maintain the ctx
(or options
, if you will) as the first positional argument, then keyword arguments.
Double splat operator **
at the end will be required to in order to discard unused kw args.
You can introduce this change before actual migration to 2.1.
Steps declared as success
or failure
are now renamed to pass
and fail
respectively.
class Memo::Create < Trailblazer::Operation
success :model
failure :handle_some_error
end
Change it as follows.
class Memo::Create < Trailblazer::Operation
pass :model
fail :handle_some_error
end
If you are using Rubocop it will probably start complaining about unreachable code because it just so happens that fail
is also a Ruby Kernel’s method. One solution to this could be to add a custom rule to .rubocop.yml
like this:
Lint/UnreachableCode:
Exclude:
- '*/**/concepts/**/operation/**/*'
Style/SignalException:
Exclude:
- '*/**/concepts/**/operation/**/*'
There is also a trick
that will allow you to do this rename before actually migrating to 2.1. You can put this in an initializer:
Trailblazer::Operation::Pipetree::DSL.send(:alias_method, :pass, :success)
Trailblazer::Operation::Pipetree::DSL.send(:alias_method, :fail, :failure)
This way you could introduce this change before actual migration to 2.1. Just don’t forget to remove it after updating gems to 2.1.
Now every step that may end up in pass_fast
or fail_fast
signals need an extra option that indicates fast track usage. Change this operation:
class Memo::Create < Trailblazer::Operation
step :create
def create(ctx, **)
Railway.pass_fast! if ctx[:model].save
end
end
Use the :fast_track
option to let Trailblazer know about the potential new signal being emitted.
class Memo::Create < Trailblazer::Operation
step :create, fast_track: true # notice the extra option :fast_track
def create(ctx, **)
Railway.pass_fast! if ctx[:model].save
end
end
Context
The keys for ctx
used to be mixed up, some where "longer.strings"
, some were :symbols
. The new context implementation Context::IndifferentAccess
now allows to use both.
ctx["model"]
ctx[:model] # => same!
This also works for namespaced keys, which you still might find helpful.
ctx["contract.default"]
ctx[:"contract.default"] # => same!
On the core level, we use symbol keys, only (e.g. :"contract.default"
).
The default implementation of the context object can be set by overriding Context.implementation
. For example, if you want the old behavior back.
class Trailblazer::Context
def self.implementation
Context # old behavior.
end
end
Note that the override might be deprecated in favor of a dependency injection.
Nested
The Nested
macro allows to, well, nest activities or operations, providing a neat way to encapsulate and reuse logic.
In 2.1, the [Subprocess
macro] is the standard way for nesting. The Nested
macro should only be used when you use the dynamic version where the nested operation is computed at runtime using :builder
.
An exception will warn you about the inappropriate usage.
[Trailblazer] Using the `Nested()` macro with operations and activities is deprecated. Replace `Nested(Create)` with `Subprocess(Create)`.
Both the :input
and :output
options that used to go with Nested(Present, :input: ...)
are now a generic option in Trailblazer. Move them behind the
macro parenthesis.
# 2.0
Nested(Present, input: :my_input, output: :my_output)
# 2.1
Nested(Present), input: :my_input, output: :my_output
An exception will stop compilation if you fail to obey.
ArgumentError: unknown keyword: input
Behold of another tiny API change! The :output
filter signature has changed for a better.
In Trailblazer 2.0, the following signature was allowed.
# 2.0
Nested(Present,
output: ->(ctx, mutable_data:, **) {
{model: mutable_data[:article]} # you used the {mutable_data} keyword argument.
}
)
Mind the arguments being passed to :output
. The first positional is the original outer context, the context from the nested operation comes as a mutable_data:
keyword argument.
In 2.1, those two have swapped.
# 2.1
Nested(Present),
output: ->(inner_ctx, article:, **) {
# {inner_ctx} is {mutable_data}
{model: article} # you used the {mutable_data} keyword argument.
}
The inner context from the nested operation comes first, as a positional argument. Note how you can conveniently use keyword arguments to access variables from this inner ctx (e.g. article:
). Keep in mind, the naming is completely up to you. We use inner_ctx
/ctx
and original_ctx
in our code.
If you also need the original context in the :output
filter, use the :output_with_outer_ctx
option.
# 2.1
Nested(Present),
output_with_outer_ctx: true
output: ->(inner_ctx, original_ctx, article:, **) {
{
model: article,
errors: original_ctx[:errors].merge(inner_ctx[:errors]) # you can access the outer ctx!
}
}
The original, outer ctx comes in as a second positional argument.
You do not need to extend Uber::Callable
in classes with #call
methods anymore.
Check the Variable Mapping docs for a complete explanation.
Don’t forget to declare fast track usage if you expect it from within nested operation, like this:
Nested(Present), input: :my_input, output: :my_output, fast_track: true
Another difference is that in 2.0, when you were using pass_fast
in nested operations, it would stop only the nested operation from executing. After this the outer one would continue executing.
Now returning pass_fast
in nested operation will stop both, inner and outer operation with success
as a final result. If you rely on old behaviour you can still have it with proper signals mapping:
Nested(Present), input: :my_input,
output: :my_output,
fast_track: true,
Output(:pass_fast) => Track(:success), # pass_fast now mapped to `just` a success
Output(:fail_fast) => End(:failure)
Macro API
Macros are functions that add a task along with specific options to the activity. In TRB 2.0, those (historically camel-cased) functions returned an array with two elements.
module MyMacro
def self.NormalizeParams(name: :myparams, merge_hash: {})
task = ->((ctx, flow_options), _) do
ctx[name] = ctx[:params].merge(merge_hash)
return Trailblazer::Activity::Right, [ctx, flow_options]
end
[task, name: name] # old API
end
end
In 2.1, a hash is returned. Note that :name
is :id
now.
module MyMacro
def self.NormalizeParams(name: :myparams, merge_hash: {})
task = ->((ctx, flow_options), _) do
ctx[name] = ctx[:params].merge(merge_hash)
return Trailblazer::Activity::Right, [ctx, flow_options]
end
# new API
{
task: task,
id: name
}
end
end
This allows for a much richer macro experience where you might add additional steps via a macro, use DSL options such as :before
and :after
and add taskWrap extensions. [macro API]
A common error if you don’t return a hash from your macro is a call
on Array
.
NoMethodError:
undefined method `call' for #<Array:0x000055663116be98>
# gems/trailblazer-context-0.4.0/lib/trailblazer/option.rb:9:in `call!'
In Trailblazer 2.0, macros were automatically using the task interface. Mind the arguments and the return value.
task = ->(ctx, current_user:, model:, **) {
# do something
# return true/false
}
In Trailblazer 2.1, macros use the circuit interface per default. If you want the simpler task interface, you need to wrap your task using Binary()
.
def MyMacro()
task = ->(ctx, current_user:, model:, **) {
# do something
# return true/false
}
{
task: Trailblazer::Activity::TaskBuilder.Binary(task), # wrap the callable.
id: "my_macro"
}
A common error if you don’t wrap your macro is an ArgumentError
.
ArgumentError:
missing keywords: current_user, model
In case you used the same macro twice in one operation, like this for example:
class Create < Trailblazer::Operation
step Contract::Persist(method: :sync)
step Contract::Persist(method: :save!)
end
You will have to specify id explictly for one of them:
class Create < Trailblazer::Operation
step Contract::Persist(method: :sync)
step Contract::Persist(method: :save!), id: "some_custom_unique_id"
end
Contract DSL
It was possible to define contracts on the operation level using a DSL.
class Create < Trailblazer::Operation
contract do
property :id
end
step Contract::Build()
step Contract::Validate()
end
Since the usability doesn’t outweigh the complexity needed to implement such DSL, we decided to remove that functionality for now.
Instead, use an explicit inline class and the :constant
option.
class Create < Trailblazer::Operation
class Form < Reform::Form
property :id
end
step Contract::Build(constant: Form)
step Contract::Validate()
end
Trailblazer loader
Usage of a trailblazer-loader is now discouraged as it’s slower than the ones provided by Rails and it’s error prone.
In short, we decided to adopt the Rails naming scheme and change operation names from User::Create
to User::Operation::Create
, so the file name and class path are in sync.
Read the details here.
We highly recommend changing this with your upgrade as it highly improves the dev experience.
Developer
Trailblazer provides a rich set of developer tools to ease debugging and make modelling processes a pleasant experience.
The developer
gem contains tools to help you visualize and debug Trailblazer code. Its development features such as tracing or exception handling are advanced tools and will become your best friends very quickly.
Constant
We advise you to alias the Developer
constant to something handy, such as ::Dev
in your project. For instance, in Rails you’d have the following line in a config/initializers/trailblazer.rb
initializer.
# config/initializers/trailblazer.rb
require "trailblazer/developer"
Dev = Trailblazer::Developer
Wtf?
Use wtf?
if you want to
- Debug an exception happening somewhere deep in your code.
- Find out which step changed the track to failure.
- Focus on specific step(s) to find out what
ctx
mutations are happening inside them.
This will run your activity with tracing enabled, and in case of an exception, print the trace path to the closest point where the exception was thrown.
Your activity will be called using the TaskWrap
invoker, a possible exception will be caught and the closest task found. It then prints the “stack trace”, however, on a much higher level based on tasks, not methods.
Please note that this is a higher-level debugging tool that does not confront you with a 200-lines stack trace the way Ruby does it, but pinpoints the exceptional code and locates the problem on a task level.
This is possible due to you structuring code into higher abstractions, tasks and activities.
You can focus on specific steps and variables to find out what ctx
comes in and what goes out. The focus_on
option allows us to capture any key(s) from ctx
and print any mutations happening within given steps.
You need to pass one or more step names (either default name or given by explicit :id
) to capture the mutations.
class Memo::Create < Trailblazer::Activity::Path
step :validate
step :create, id: :create_memo
# ...
end
Dev.wtf?(Memo::Create, [ctx, { focus_on: { steps: [:validate, :create_memo] } }])
By default, focusing will capture whole ctx
for given steps. But you can also filter on it by passing one or more keys using variables
.
Dev.wtf?(Memo::Create, [ctx, { focus_on: { variables: [:params], steps: :validate } }])
Internally, when focus_on
option is passed, we call Object#inspect
method to collect before-after data from the ctx
for given steps. This data is then used while rendering the trace. In case you want to customize the data collection behaviour with your own logic, you can pass the default_inspector
.
Dev.wtf?(
Memo::Create,
[
{ params: { text: 'Hydrate!', value: nil } },
{
focus_on: { steps: :validate, variables: :params },
default_inspector: ->(value){ value.nil? ? 'UNKNOWN' : value.inspect }
}
]
)
Inspection runs deeply when ctx
contains nested structures like hashes or arrays. It’s concept is very simple and handled in Dev::Trace::Inspector
module.
The sole purpose to add Dev::Trace::Inspector
module is to make custom inspection possible and efficient while tracing. For example, ActiveRecord::Relation#inspect
makes additional queries to fetch top 10 records and generate the output everytime.
To avoid this, Inspector
will not call inspect
method when it finds such objects (deeply nested anywhere).
Instead, it’ll call AR::Relation#to_sql
to get plain SQL query which doesn’t make additional queries and is better to understand in tracing output. There is always a possibility to cater such cases and make wtf
tracing better. Contributions are welcome :)
The color_map
option allows you to customize default coloring scheme being used to render passed or failed steps.
Render
It often is helpful to visualize an activity. One way is the render
helper.
puts Trailblazer::Developer.render(Memo::Create)
The rendered output prints each task, its outputs and where they’re connected to.
We’re working on improved visualization for the console, and for the web editor. If you want to help, please ping us on our chat.
Render Linear
Client
The Developer::Client
module provides functions to work with activities and workflows created in the PRO editor.
Notes
- IDs are extracted from the label of the element. They get
chomp
ed to remove a potential newline at the end and reduce confusion. - It is currently not possible to assign semantics to arrows via the label. In the meantime, use the
:query
option. # TODO
Context
Context aka ctx
(or plain old options
) is a core argument-specific data structure for Trailblazer. It provides a generic, ordered read/write interface that collects mutable runtime-computed data while providing access to any compile-time information. It is extracted into its own gem and can also be used independently.
ctx
ctx
can be initialized when an operation is invoked at the run time or by defining dependencies at the compile time. Inside the operation, it gets passed down to every step
with it’s argument position depending on step’s interface. It will contain whatever the most recently executed step has changed and hopefully contains what you’re expecting.
If you want to see what ctx
modifications are being performed per step or at specific steps, you can debug it using developer’s focus_on API.
ctx
’s purpose is to hold the state of your activity which can also be passed down to other nested activities using Subprocess. You can filter what such activities can or can not “see” using variable mapping. After operation’s execution using public call, the Result object wraps the context for convenient access.
In order to provide the generic interface, scoping and debugging capabilities, the “Hash” argument you provide to an operation is initialized as an instance of Trailblazer::Context::Container
to build up the final ctx
. This allows us to support more features on top of it like indifferent access, aliasing etc
ctx
mimics as “Hash” and also allows you to use Strings or Symbols interchangeably as keys; similar to the params
hash in Rails.
result = Memo::Create.(params: { text: "Enjoy an IPA" })
result[:params] # => { text: "Enjoy an IPA" }
result['params'] # => { text: "Enjoy an IPA" }
All keys are stored as Symbols by default in order to allow them to be accessible as keyword arguments.
Note that ctx
doesn’t provide interchangeable keys for any nested hashes because of the performance reasons.
Most commonly found keys in ctx
are 'contract.default', 'contract.default.params', 'policy.default'
etc. It sometimes becomes cumbersome to access them from ctx
as they can’t be defined as keyword arguments in steps.
To overcome this, it is possible to define a shorter versions of context keys using aliases
. By providing aliases mapping in flow_options[:context_options]
, context will maintain any mutations being made on the origianl keys with
the aliases and vice versa.
options = { params: { text: "Enjoy an IPA" } }
flow_options = {
context_options: {
aliases: { 'contract.default': :contract, 'policy.default': :policy },
container_class: Trailblazer::Context::Container::WithAliases,
}
}
# Sorry, this feature is only reliable in Ruby > 2.7
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
result = AliasesExample::Memo::Create.(options, flow_options)
else # Ruby 2.6 etc
result = AliasesExample::Memo::Create.call_with_flow_options(options, flow_options)
end
result['contract.default'] # => Memo::Contract::Create
result[:contract] # => Memo::Contract::Create
flow_options
are passed to the nested operations via Subprocess
and all given aliases will also be applied in them by default.
class Memo::Create < Trailblazer::Operation
# ...
pass :sync
def sync(ctx, contract:, **)
# ctx['contract.default'] == ctx[:contract]
contract.sync
end
end
Option
Trailblazer::Option is one of the core concept behind operation’s step API, reform’s populator API etc. It makes us possible to accept any kind of callable objects at compile time and execute them at runtime.
class Song::Create < Trailblazer::Operation
step Authorize # Module callable
step :model # Method callable
step ->(ctx, model:, **) { puts model } # Proc callable
end
It is also a replacement over declarative-option and has been extracted out from trailblazer-context by identifying common callable patterns.
Callables
Trailblazer::Option()
accepts Symbol
, lambda
and any other type of callable as an argument. It will be wrapped accordingly to make an executable, so you can evaluate it at runtime.
Passing a Symbol
will be treated as a method that’s called on the given exec_context
.
option = Trailblazer::Option(:object_id)
option.(exec_context: Object.new) # => 1234567
Same with the objects responding to .call
or #call
method.
class CallMe
def self.call(message:, **options)
message
end
end
option = Trailblazer::Option(CallMe)
option.(keyword_arguments: { message: "hello!" }, exec_context: nil) # => "hello!"
Notice the usage of keyword_arguments
while calling an Option()
. They need to be mentioned explicitly in order for them to be compatible with ruby 2.7+.
And of course, passing lambdas. They get executed within given exec_context
when set.
option = Trailblazer::Option(-> { object_id })
option.(exec_context: Object.new) # => 1234567