Carve your controllers like Papa Carlo
Tags: rails, ruby, showdev
Published: 2019-02-12 16:57:32

---
title: Carve your controllers like Papa Carlo
published: true
canonical_url: https://evilmartians.com/chronicles/rubanok-carve-your-rails-controllers-like-papa-carlo
description: Slimming down Rails controllers using Rubanok gem
tags: rails, ruby, showdev
---

We, Rails developers, tend to keep our controllers _skinny_ (and [models fat](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model)–oh, wait, [it's](https://blog.makersacademy.com/forget-fat-models-its-time-for-skinny-controllers-and-skinny-models-a9b84ec481b7) [not true](https://medium.com/@devlob/fat-models-and-skinny-controllers-well-not-exactly-918585b02348) [anymore](https://www.rubytapas.com/2017/04/11/skinny-controller-domain-model-events/); now we have [fat services](https://medium.com/marmolabs/skinny-models-skinny-controllers-fat-services-e04cfe2d6ae) 😉).

We add different layers of abstractions: [interactors](https://github.com/collectiveidea/interactor), [policies](https://github.com/palkan/action_policy), query objects, [form objects](https://github.com/trailblazer/reform), you name it.

And we still have to write something like this when dealing with query params-based filters:

```ruby
class EventsController < ApplicationController
  def index
    events = Event.all.
      page(params[:page] || 1).
      order(sort_params)

    events = events.where(
      type: params[:type_filter]
    ) if params[:type_filter].in?(%w[published draft])

    events = events.future if params[:time_filter] == "future"
    # NOTE: `searched` is a scope or class method defined on the Event model
    events = events.searched(params[:q]) if params[:q].present?

    render json: events
  end

  def sort_params
    sort_by = params[:sort_by].in?(%w[id name started_at]) ?
              params[:sort_by] :
              :started_at
    sort_order = params[:sort].in?(%w[asc desc]) ? params[:sort] : :desc
    { sort_by => sort_order }
  end
end
```

Despite having a non-skinny controller, we have the code which is hard to **read**, **test** and **maintain**.

I want to show how we can _carve_ this controller (just like _Papa Carlo_ carved _Buratino_–[Russian Pinocchio](https://en.wikipedia.org/wiki/Buratino)–from a log) using a new gem–[Rubanok][] (which means "hand plane" in Russian).

[Rubanok][] is a general-purpose tool for data transformation driven by Hash-based params.

Ok, that sounds weird 😕

Let just look at our example above: we take our _data_ (Active Record relation, `Event.all`) and transform it according to the user's _input_ (`params` object).

What if we could extract this _transformation_ somewhere out of the controller?

You may ask: "What's the point of this abstraction"?

There are [several reasons](https://gemcheck.evilmartians.io):
- Make our code more readable (less logic branching)
- Make our code easier to test (and [make tests faster](https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests))
- Make our code reusable (e.g., sorting and pagination logic is likely used in other controllers, too).

Let me first show you how the above controller looks when we add Rubanok:

```ruby
class EventsController < ApplicationController
  def index
    events = planish Event.all
    render json: events
  end
end
```

That's it. It couldn't be slimmer (ok, we can make `render json: planish(Event.all)`).

What's hidden under the `planish` method?

It's a Rails-specific method (btw, Rubanok itself is **Rails-free**) that utilizes convention over configuration principle and could be unfolded into the following:

```ruby
def index
  events = EventsPlane.call(Event.all, params.to_unsafe_h)
  render json: events
end
```

And the `EventsPlane` class is where all the ~~magic~~ transformation happens:

```ruby
class EventsPlane < Rubanok::Plane
  TYPES = %w[draft published].freeze
  SORT_FIELDS = %w[id name started_at].freeze
  SORT_ORDERS = %w[asc desc].freeze

  map :page, activate_always: true do |page: 1|
    raw.page(page)
  end

  map :type_filter do |type_filter:|
    next raw.none unless TYPES.include?(type_filter)
    
    raw.where(type: type_filter)    
  end

  match :time_filter do
    having "future" do
      raw.future
    end
    
    default { |_time_filter| raw.none }
  end

  map :sort_by, :sort do |sort_by: "started_at", sort: "desc"|
    next raw unless SORT_FIELDS.include?(sort_by) &&
      SORT_ORDERS.include?(sort)
    raw.order(sort_by => sort)
  end

  map :q do |q:|
    raw.searched(q)
  end
end
```

The _plane_ class describes how to transform data (accessible via `raw` method) according to the passed `params`:
- Use `map` to extract key(-s) and apply a transformation if the corresponding values are not empty (i.e., empty strings are ignored); and you can rely on Ruby keyword arguments defaults here–cool, right?
- Use `match` take values into account as well when choosing a _transformer_.

Now we can write tests for our _plane_ in isolation:


```ruby
describe EventsPlane do
  let(:input) { Event.all }
  # add default transformations
  let(:output) { input.page(1).order(started_at: :desc) }
  let(:params) { {} }

  # we match the resulting SQL query and do not make real queries
  # at all–our tests are fast!
  subject { described_class.call(input, params).to_sql }

  specify "q=?" do
    params[:q] = "wood"

    expect(subject).to eq(output.searched("wood").to_sql)
  end

  specify "type_filter=<valid>" do
    params[:type_filter] = "draft"

    expect(subject).to eq(output.where(type: "draft").to_sql)
  end

  specify "type_filter=<invalid>" do
    params[:type_filter] = "unpublished"

    expect(subject).to eq(output.none.to_sql)
  end

  # ...
end
```

In your controller/request test all you need is to check that a specific plane has been used:

```ruby
describe EventsController do
  subject { get :index }

  specify do
    expect { subject }.to have_planished(Event.all).
      with(EventsPlane)
  end
end
```

So, Rubanok is good for _carving_ controllers, but we said that it's general-purpose–let's prove it with GraphQL example!

```ruby
module GraphAPI
  module Types
    class Query < GraphQL::Schema::Object
      field :profiles, Types::Profile.connection_type, null: false do
        argument :city, Int, required: false
        argument :home, Int, required: false
        argument :tags, [ID], required: false
        argument :q, String, required: false
      end

      def profiles(**params)
        ProfilesPlane.call(Profile.all, params)
      end
    end
  end
end
``` 

It looks like we've just invented _skinny types_ 🙂

Check out [Rubanok](https://github.com/palkan/rubanok) repo for more information and feel free to propose your ideas!

**P.S.** There is an older gem [`filterer`](https://github.com/dobtco/filterer) which implements a similar idea (though in PORO way), but focuses on ActiveRecord and lacks testing support.

**P.P.S.** Wondering what other abstractions we use to organize code in large applications? Check out my other posts, such as ["Crafting user notifications in Rails with Active Delivery"](https://dev.to/evilmartians/crafting-user-notifications-in-rails-with-active-delivery-5cn6) or ["Clowne: Clone Ruby models with a smile"](https://evilmartians.com/chronicles/clowne-clone-ruby-models-with-a-smile), and projects, such as [Action Policy](https://actionpolicy.evilmartians.io) and [Anyway Config](https://github.com/palkan/anyway_config).

---

Read more dev articles on [https://evilmartians.com/chronicles](https://evilmartians.com/chronicles)!
[Rubanok]: https://github.com/palkan/rubanok