A Little Trailblazer Cookbook > stdout.in Ievgen Kuzminov IT blog

A Little Trailblazer Cookbook

Jul 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.

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 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
  ...
comments powered by Disqus
Ievgen
Kuzminov "iJackUA"
Web Team Lead
at MobiDev (Kharkiv, Ukraine)
Code in Ruby and Elixir, but still love PHP. Explore ES6 and Vue.js. Explore databases, use Ubuntu and MacOS, think about IT people and management

Notes


Framework functionality that is not related to merely taking input from a Request and presenting output through a Response becomes entirely secondary to, perhaps even actively harmful to, building well-structured applications... [more]