Carve your controllers like Papa Carlo


Share this post on


If you’re interested in translating or adapting this post, please contact us first.

We, Rails developers, tend to keep our controllers “skinny” (and models “fat”—oh, wait, it’s not true anymore; now we have “fat” services 😉).

We add different layers of abstractions: interactors, policies, query objects, form objects, you name it.

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

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

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

    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

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

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”—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:

  • Make our code more readable (less logic branching)
  • Make our code easier to test (and make tests faster)
  • 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:

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

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:

def index
  events =, params.to_unsafe_h)
  render json: events

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

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|

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

    raw.where(type: type_filter)

  match :time_filter do
    having "future" do

    default { |_time_filter| raw.none }

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

    raw.order(sort_by => sort)

  map :q do |q:|

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:

describe EventsPlane do
  let(:input) { Event.all }
  # add default transformations
  let(:output) { :desc) }
  let(:params) { {} }

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

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

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

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

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

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

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

  # ...

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

describe EventsController do
  subject { get :index }

  specify do
    expect { subject }.to have_planished(Event.all)

So, Rubanok is good for carving controllers, but we said that it’s general-purpose—let’s prove it with GraphQL example!

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

      def profiles(**params), params)

It looks like we’ve just invented skinny types 🙂

Check out Rubanok repo for more information and feel free to propose your ideas!

P.S. There is an older gem 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” or “Clowne: Clone Ruby models with a smile”, and projects, such as Action Policy and Anyway Config.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

In the same orbit

How can we help you?

Martians at a glance
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at