A Little Trailblazer CookbookJul 14, 2016, 2:18:33 PM
Trailblazer is a high-level Architecture libraries for the Ruby web application (not only for Rails). If you are no familiar with it yet - take a 20 minutes walk through guide...
I want to cover once again those places where our team had issues or misunderstanding. Trailblazer documentation got a lot of improvements recently and keep getting more and more care. Some points from this list are covered by the docs, but in practice not everything was smooth.
Don't try to fit all operations into CRUD. Like this...
include Model model MyModel, :create
you can define an arbitrary model structure by overriding
It could be custom finder
def model!(params) MyModel.find_by_super_magic(params['voila']) end
or even pass you model externally
def model!(params) params[:model_object] end
Change/add params before validation
When you need to modify param or create a new based on input params. The best place to do it before validation is in
def setup_params!(params) super params[:port] = port_from_host(params[:host]) if params[:host] params end
don't forget to add check
if params[:host] if your input param is optional.
Or right before
validate call in
def process(params) params[:port] = port_from_host(params[:host]) if params[:host] validate(params) do |f| ... end end
Don't forget SUPER
In methods like
setup_params! it is very important to remember to add
def setup_params!(params) super ...
Or you will have very hard times debugging the side effects.
This part of docs was very helpful for me to understand what is the Operation initialization flow.
::call ├── ::build_operation │ ├── #initialize │ │ ├── #setup! │ │ │ ├── #assign_params! │ │ │ │ │ ├── #params! │ │ │ ├── #setup_params! │ │ │ ├── #build_model! │ │ │ │ ├── #assign_model! │ │ │ │ │ ├── #model! │ │ │ │ ├── #setup_model! │ ├── #run │ │ ├── #process
Trbrb documentation and book uses such an approach in declaring operation class.
class Message < ActiveRecord::Base class Create < Trailblazer::Operation ....
It requires this weird
< ActiveRecord::Base declaration. But it is needed only when you have
Message ActiveRecord model declared. In such case, if you omit
< ActiveRecord::Base in operation it will raise an error that class declaration differ from initial.
It works pretty except that is looks weird, and sometimes it just does not work and complains about wrong inheritance declaration during code reload. To eliminate this issue I provide I rule to add a core namespace
module Core module Message class Create < Trailblazer::Operation
A little bit verbose, but clearer as for me.
Make separate files for each Cell and Operation
Just do it :) Use trailblazer-cells to have separate view files.
It will be easier to navigate all these classes when the project is growing!
Inherit multiple contracts
When you have a long Contract - separate it into modules. When you inherit
Update operation from
Create - it inherits Contract by default.
Imagine we have
Create separate common Create and Update Contracts
module Post::Contract module Create include Reform::Form::Module property :a, default: "Oh, that's A" property :b
and forbid possibility to edit property
b while Update, for example
module Post::Contract module Update include Reform::Form::Module property :b, writable: false
module Core module BasicPost class Create < Trailblazer::Operation ... contract do include ::Post::Contract::Create end
module Core module BasicPost class Update < Create ... contract do include ::Post::Contract::Update end
What about AdvancedPost that reuses BasicPost and adds it's own fields?
Do not inherit Advanced Create from Basic create - it causes duplication of rules and issues in Update operation. Inherit it from plain Operation class and use Composition.
module Core module AdvancedPost class Create < Trailblazer::Operation .... contract do include ::Post::Contract::Create property :c, default: 80
module Core module AdvancedPost class Update < Create ... contract do include ::Post::Contract::Update property :c, default: 80, writeable: false end
Yes, it requires a lot of control, but in the end, it works as expected!
trailblazer_loader gem does a great job by requiring you classes in expected order (btw, you better read about the order :)), but sometimes with complex Operations inheritance you could face an issue that some Concept class is not loaded before it's "child".
Do not invent anything, just do explicit
Contract is a Twin
Don't forget that Contract is a Twin. So all the great thigs like a composition,
nilify etc. could be used. And these docs helps a lot.
Property is not the same in Contract, Cell, and Representable
Despite Operation, Cell and Representable have a very similar syntax of
property declaration. They are not the same. And it is a little confusing.
- Operation's Contract
propertyis derived from a Twin.
propertyis just a direct call delegation to the
model, no Twin tricks are available. Because Cell is not meant to transform or parse input parameters.
- Representable uses it's own Shema handling and you should not try to find parallels with Contract definition.
Use decorator lib like Draper
Twin could serve as a decorator, but for me, it seems a bit overloaded with all that
sync functionality (as for read-only decorator). So I use Draper to keep all reusable decorations for a Model.
Simple rule decorates Models only when you really need decoration methods.
If you need decoration methods inside the operation, DO NOT DO this:
def setup!(params) super @model = @model.decorate end
You will end up with exceptions in really unexpected places (while operation tries to sync data). Let the operation use Model, and use Decorated model only in places where logic (based on decorator) is performed.
If you need the decorated Model in all views (that is very likely), you can make this trick
class ApplicationController < ActionController::Base # ... # Here should be all your code from root Controller class # ... private # Override default Trbrb model assignment def setup_operation_instance_variables!(operation, options) super # reassign @model with decorated one (this example is for Draper) @model = @model.decorate if !@model.nil? && @model.decorator_class? end end
Kaminari and html_safe
When you add a Kaminari pagination inside a Cell and use generated templates, it will render as escaped HTML text.
Because Kaminari uses a special classes for each tag, I got this solution. Add
.to_s.html_safe in places where you render Kaminiri tags.
prev_page_tag.to_s.html_safe next_page_tag.to_s.html_safe page_tag(page).to_s.html_safe ...