Trailblazer is a collection of gems to help you structure growing Ruby applications. It does so by providing a higher level of architecture through new abstractions.
The layers we provide are designed to be stand-alone and do not require you buying the full TRB stack.
Are you new to TRB? Then it’s probably best to start with our LEARN section and find out more about this framework!
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.
Trailblazer empowers developers to build better applications. By “better” we mean maintainable, stable and, yes, fun to work with!
Our approach enforces encapsulation by providing new abstraction layers. It maximizes testability and reusability, and simplifies onboarding of new developers as we have strong conventions, thorough docs and screencasts, and a vivid community.
Unlike other “modern architecture” approaches in the Ruby community (as seen in numerous blog posts and books) neither do we tell you how to apply a new, fancy design pattern in your project by giving you suggestions for vague “best practices”, nor do we instruct how to introduce that new, self-tailored service object. No. we give you battle-tested functions, abstractions and classes that enforce a clean architecture.
We firmly believe that the utilization of ready-to-use layers will always prevail over a documentation-driven approach. In the latter, reliance on constantly outdated “architecture docs” creates a continuous struggle for a designated senior to document the process of creating a specific type of object.
Believe it or not, but you can learn about Trailblazer step-wise! It’s usually smart to start checking out operations, then form gems using Reform, and after that macros and Rails integration.
For a while we’ve been pushing very short 5-min videos teaching you how operations work, what testing looks like and which features they bring for your daily development.
Producing those instructional screencasts turned out to be a lot of fun (and a bit of work), so expect a new episode every few weeks. This is definitely much simpler than writing books!
Grab a coffee and watch the first few episodes to quickly grasp how to master this indispensable tool to structure your business logic.
The Trailblazer - A new architecture for Rails book from 2017 is very outdated and discusses Trailblazer 1.x! However, the first two chapters are worth a read, if you want to pick up the spirit.
It’s free to download.
The Building your own authentication library with Trailblazer book, however, is up-to-date. It’s missing part II which discusses Rails integration, but gives you a nice tour through operations.
Everything discussed in the “Buildalib” book is also covered in our videos.
Here’s the link to the “Rails Basics” tutorial.
We are revisiting our tutorial section in 2024.
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
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.
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)
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
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
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.
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.
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
The iconic wtf?
method is one way to invoke an operation with tracing turned on. During the execution, invoked steps are traced and printed in the console as a tree-view, helping you to understand the code path the operation took. It is one of Trailblazer’s most-loved features and was introduced in Trailblazer 2.1.
When working with a Trailblazer::Operation
simply invoke its #wtf?
method.
result = Memo::Operation::Create.wtf?(params: {title: "Remember me..."})
On the console, you can see the printed trace in all its beauty.
This method is especially helpful if you want to
ctx
mutations are happening inside them.In case of an exception somewhere in the operation or its nested activities, wtf?
will print the trace path to the closest point where the exception was thrown.
The original exception will be visible on the console, too.
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.
The #wtf?
class method is only available for Trailblazer::Operation
subclasses. You will get an exception if you try to use it with Trailblazer::Activity::Railway
and other activities.
NoMethodError: undefined method `wtf?' for Memo::Operation::Create:Class
However, tracing can be used with low-level Activity
classes, too.
module Memo::Operation
class Create < Trailblazer::Activity::Railway # Note that this is not an {Operation}!
step :extract_markdown
step :model
# ...
end
end
You have to use the clumsier Developer.wtf?
method for tracing.
signal, (ctx, _) = Trailblazer::Developer.wtf?(
Memo::Operation::Create, [{params: {title: "Remember me.."}}, {}]
)
As always with Activity
subclasses, it operates with the circuit interface.
The color_map
option allows you to customize default coloring scheme being used to render passed or failed steps.
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.
The Developer::Client
module provides functions to work with activities and workflows created in the PRO editor.
Notes
chomp
ed to remove a potential newline at the end and reduce confusion.:query
option. # TODOWith trailblazer-activity
0.16.0, the Introspect::Graph
API has been moved to trailblazer-developer
and might get replaced. Please refer to the Introspect::Nodes
API.
To find out the structure and composition details about any activity, use the Introspect
API.
You may use Graph#find
with a block for a more complex search.
node = graph.find { |node| node.task.class == Trailblazer::Activity::TaskBuilder }
Alternatively, using the Graph#find
method with an ID provided, you can retrieve a particular node in the activity’s circuit.
Consider the following activity.
class Memo::Update < Trailblazer::Activity::Railway
step :find_model
step :validate, Output(:failure) => End(:validation_error)
step :save
fail :log_error
end
You can introspect a certain element by using find(id)
.
graph = Trailblazer::Developer::Introspect.Graph(Memo::Update)
node = graph.find(:validate)
Note that all queries go via a Graph
instance.
The returned node instance exposes several inspect helpers.
The ID of the element is retrieved by using #id
.
# puts node.id.inspect #=> :validate
To see what actual task sits behind this circuit’s node, use #task
. This is always the low-level task that responds to a circuit interface. It might be a wrapper around your actual logic, provided by the DSL.
# puts node.task #=> #Trailblazer::Activity::TaskBuilder::Task user_proc=validate>
Outgoing connections from a task can be queried using #outgoings
. This returns an array of all outgoing arrows.
# left, right = node.outgoings # returns array
An Outgoing
provides several interesting fields.
You can retrieve the connected element by using #task
. It returns the actual low-level task.
# puts left.task #=> #Trailblazer::Activity::End semantic=:validation_error>
The #output
method returns the Output
instance that connects the task to the next element.
# puts left.output.signal #=> Trailblazer::Activity::Left
# puts left.output.semantic #=> :failure
The Output#signal
field returns the signal object the task returns to trigger that connection. To see the semantic of this output, use Output#semantic
.
The Node#outputs
method returns an array of Output
objects defining each output of outgoing connections of a specific node.
outputs = node.outputs
left = outputs[0] #=> output object