Is your app deRailing?

Everything started out smoothly, building a MVP in a few days is amazing, but after some months of non-stop development it just isn't that simple to implement that great new feature. Your models are full of validations, the controllers are calling methods from another galaxy, everything seems to be tested but bugs keep on rising, like zombies from an apocalyptic (not so distant) future.

Ok. This might been a bit too harsh, but I really felt a bit like that.

The bottom-line was: we were really unhappy with the lack of structure in a vanilla Rails Way. Besides some conventions about database table names and directories for god classes called controller or model, Rails didn't give us any other guidance. So, we started looking around for architectures, or better, gems that enforce architectures and not just talk about how things could be done.

That's when I met Trailblazer, it's a "gem" idealized and created by Nick Sutterer aka @apotonick, yes he's the guy from cells.

Trailblazer is an architecture based on Operations, those are the entry-points of our app, they will define the "internal" API of your application. So it helps you to give back to your models and controllers theirs original responsibilities. A controller shouldn't really know about the underlying domain model, in the same way your Model shouldn't care about end user validations or crazy business logic, models are entities that define what are the data that you can work with. So, what trailblazer give us are some of those "missing" layers on top of the basic MVC stack.

Here's how an Operation looks like:

class Post::Create < Trailblazer::Operation  
  contract do
    property :title, validates: { presence: true }
    property :body, validates: { presence: true }
  end

  def process(params)
    validate(params[:post], model) do
      contract.save
    end
  end

  private

  def model!
    Post.new
  end
end  

The contract block specifies the properties used by this operation, note that any other :property not described in there will just be ignored and won't be saved to the model. That means that you don't need params.permit(:something) anymore. #winwin

The #process methods receive a hash of params and "processes" them. In our case we want to validate our post params and if everything is ok, save them to our model.

Fine, that's cool, but how do I use those "operations" of yours?

Easy:

class PostsController < ApplicationController

  # other actions...

  def create
    Post::Create.run(params) do |op|
      return redirect_to posts_path
    end

    render :new
  end
end  

The great thing here is that our controller actions doesn't need to care about any business logic, they sole role is to call an Operation and operate on its result, that is: if everything went fine we want to redirect our users to the post index page, but if something went wrong we will re-render the :new view.

But dude, I could've done that with an usual if @model.valid? call in my action. Yes you could, but controllers shouldn't be dealing with business logic, what if you wanted to publish! a post whenever it's saved? You would need to add that into the action, and that's not cool. Controllers are there to handle HTTP stuff, they are great for rendering, redirecting, etc.

Another benefit is that operations use Form Objects by default, so we can exterminate all validations in our models. The contract block of an Operation uses reform to create our Form Object, it's responsible for validating and "syncing" data to our model.

class Admin::Post::Create < Post::Create  
  contract do
    # properties :title and :body 
    # are inherited from Post::Create 'contract'
    property :author, validates: { presence: true }
  end

  def process(params)
    validate(params[:post], model) do
      contract.save
      # you could do special admin stuff here if you wanted/needed
    end
  end

  # model! is inherited
end  

This Operation defines an extra attribute/property :author, both Operations use the same underlying model, but each one uses only the attributes they need, and process those attributes in their own way.

If you didn't had those separated contracts/form objects your model would probably look like that:

class Post < ActiveRecord::Base  
  attr_accessor :validate_author

  validates :title, presence: true
  validates :body, presence: true, limit: 3000
  validates :author, presence: true, if: :validate_author
end

# And 'call' our desired 'optional' validation like that
Post.new(title: 'foo',  
         body: 'bar', 
         author: '', validate_author: true).save

But since we're using a Form Object to do our validations, we can clean up our model entirely, having it nice and tidy.

class Post < ActiveRecord::Base  
  # We would only list relations here, 
  # or maybe attributes if this weren't an AR model.
end  

This separation of concerns and responsibilities also helps into creating better tests.

This is a RSpec test for our Operation:

RSpec.describe Post::Create do

  subject { described_class }

  describe "#contract" do
    # If our operation had some custom validations, coercions
    # and stuff like that we could test it here.
    # I usually don't test basic validations, 
    # they just work, but it's up to you.
  end

  describe "#process" do

    context "with valid params" do
      let(:params) { { title: "Isn't it cool?", 
                       body: "something useful here" } }

      it "persists the params" do
        res, op = subject.run(params)
        expect(op.model.persisted?).to be_truthy
        expect(res).to be_truthy
      end

      # If our operation had to generate tokens, 
      # send emails and those kind of things
      # all of them would be tested over here.
    end

    context "with invalid params" do
      let(:params) { { title: "Isn't it cool?", body: "" } }

      it "does NOT persist the params" do
        res, op = subject.run(params)
        expect(op.model.persisted?).to be_falsy
        expect(res).to be_falsy
      end
    end
  end
end  

And here is the spec for the controller using our super awesome operation.

RSpec.describe PostsController, :type => :controller do

    # Other tests ommited...

    describe "POST#create" do

      before(:each) do
        post :create, params
      end

      context "with valid params" do
        let(:params) { { title: 'foo', body: 'not foo enough' } }

        it { expect(response).to redirect_to(posts_path) }
        it { expect(flash[:notice]).to be_present }
      end

      context "with invalid params" do
        let(:params) { { customer: {} } }

        it { expect(response).to render_template :new }
        it { expect(flash.now[:alert]).to be_present }
      end
    end
end  

See, our controller spec doesn't need to care about persistence or anything business related, it knows that if an Operation succeeds it should redirect to posts_path and if not just re-render the view, and that's what we're testing.

And since we could clean up our model we don't really need a test for it xD

That's all folks!

In the next days I'll post a more in-deep step-by-step guide on using Trailblazer with Rails.

But, if you need any help or just want to chat, checkout the Trailblazer channel on gitter: https://gitter.im/trailblazer/chat

Ralf Schmitz Bongiolo

Read more posts by this author.

Amazonian Rainforest