A Little Trailblazer Cookbook
Jul 14, 2016, 2:18:33 PMTrailblazer 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.
Model
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 model!
method.
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 setup_params!
:
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 process
:
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 super
call.
def setup_params!(params)
super
...
Or you will have very hard times debugging the side effects.
Operation lifecycle
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
Namespace conflict
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 BasicPost
and AdvancedPost
.
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
Then BasicPost::Create::Operation
module Core
module BasicPost
class Create < Trailblazer::Operation
...
contract do
include ::Post::Contract::Create
end
and BasicPost::Update::Operation
is
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
and Update
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!
Require dependencies
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 require_dependency
declaration.
Contract is a Twin
Don't forget that Contract is a Twin. So all the great thigs like a composition, default
, 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
property
is derived from a Twin. - Cell's
property
is just a direct call delegation to themodel
, 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
...