Trailblazer 2.0 - What's New?
Last updated 05 May 2017Please find the complete list of changes here.
Trailblazer 2.0.0 was released in December 2016.
Installation / Structure
We will provide installation instructions as soon as the code is presentable.
The core logic has been extracted to the trailblazer-operation
gem that can be used even if you dislike Trailblazer’s semantics. But, why would you?
Additional functionality like Contract
, Policy
, etc. comes in the trailblazer
gem.
You now have to include the respective modules to extend the operation for contract, callbacks, and so on.
Call
There’s only one way to invoke an operation now: Operation::call
. You can’t instantiate using ::new
and ::run
was removed. All exceptions have been removed; call
will never throw anything unless your code is seriously broken. Instead, call
will return the result object.
In Trailblazer 1.x we defined an operation’s behavior by overriding #process
. This has been removed; operations are now defined by a new DSL using the step
and failure
methods:
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
For more information, see the guide to the new Operation API.
Params and Dependencies
You no longer merge dependencies such as the current user into params
, you can pass an arbitrary list of containers or hashes to call
, after the params
hash.
params
is treated as immutable unless you want to mess around with it.
params = { id: 1 }
Create.(params, "current_user" => Object) # just an example for the current user
Any dependency passed into the operation is called skill. Skills, or dependencies, can be accessed via #[]
.
class Create < Trailblazer::Operation
def process(params)
puts self["current_user"]
end
end
Create.(params, "current_user" => Object) #=> "Object"
Skills
The way Trailblazer 2.0 manages dependencies is extremely simple implemented and also a pleasure to use. You can assign any skill on class level you want. Note that for skills we always use string names (because we can segment them).
class Create < Trailblazer::Operation
self["contract.params.class"] = MyContract
end
Create["contract.params.class"] #=> MyContract
In call
, run-time dependencies such as the current user and class skills are made available via #[]
.
class Create < Trailblazer::Operation
self["contract.params.class"] = MyContract
def process(params)
puts self["contract.params.class"]
puts self["current_user"]
end
end
Create.({ id: 1}, "current_user" => Object)
#=> MyContract
#=> Object
You can also set skills on the instance level. This won’t override anything on the class level or other containers and is disposed of after the operation instance is destroyed.
class Create < Trailblazer::Operation
def process(params)
self["state"] = :created
end
end
Result Object
Per default, the call
method returns a result object. Currently, this is simply the immutable operation instance (it’s not really immutable, yet, but that’s easily achievable).
That makes it super simple to read all kinds of states from it.
Create.({})["state"] #=> :created
Create.({})["contract.params.class"] #=> MyContract
The API of the result object allows using it with simple conditionals. Note that this way you can expose any kind of information to the caller.
result = Create.({})
if result["state"] == :created and result["valid"]
redirect_to "/success/#{result["model"].id}"
elsif result["state"] == :updated and result["valid"]
redirect_to "/news/#{result["model"].id}"
[“model”] [“valid”] [“errors.contract”]
Pattern Matching
You can also use pattern matching with the result object.
This will help us implement generic endpoints (TO BE DOCUMENTED).
Controller methods
TRB 1 provided the controller methods run
, present
, form
, and respond
for running and/or presenting an operation. In TRB 2 these have all been removed except run
, and run
has been moved to the trailblazer-rails gem. For more information see the guides for Trailblazer::Rails.
Endpoint
Dependency Injection
The operation uses the skill mechanism to manage all its dependencies, too, such as policies, contracts, representers, and so on.
class Create < Trailblazer::Operation
include Contract
contract do
property :id
end
puts self["contract.default.class"] #=> <Contract class we just defined...>
end
This allows to inject dependencies and thus override the skill configured on the class layer.
result = Create.({ id: 1 }, "contract.default.class" => Module)
result["contract.default.class"] #=> Module, not the class-level value.
You can inject models, contract classes, policies, or whatever needs to get into the operation.
Dry-container
The skill mechanics also support injecting Dry::Container to provide additional (or all!) dependencies.
my_container = Dry::Container.new
my_container.register("user_repository", -> { Object })
Create.({}, {}, my_container)["user_repository"] #=> Object
That means that all kinds of dependencies, such as contracts or policies, can be managed by Dry’s loading and container logic.
Dry-validation Contract
Besides the fact that you now can have as many contracts as you need, Trailblazer also support Dry::Validation::Schema
contracts instead of a full-blown Reform object.
This is helpful for simple, formal validations where you don’t need deserialization, for example to validate some unrelated parts of the params
.
class Create < Trailblazer::Operation
include Contract
contract "params", (Dry::Validation.Schema do
required(:id).filled
end)
contract MyContract # the main contract
Now, have a look how to use those two contract.
def process(params)
validate(params, name: "params") do |f|
puts "params have :id!" # done with dry-validation, without Reform!
end
validate(params) do |f|
f.save # normal contract and behavior.
end
end
end
Pipetree
Example with callbacks in pipeline.
API Consistency
You might’ve noticed that APIs and object structures in Trailblazer frequently change and you might miss Rails’ consistency already.
Please keep in mind that we change things to help you building better software. When we do Trailblazer consulting, we identify design flaws together with the teams we help, and build solutions.
Those solutions are shipped as fast as we can. And that might hurt sometimes. Nevertheless, we try to ease your pain with the compat
gem, upgrading help, and we’re very confident that the 2.0 API is extremely stable and easily extended without breaking API changes.
What’s Next?
- Deserializer layer that can happen before validation, or whenever you want. This separate the deserialization/population from the actual validation and might make many people happy.