Implementing pipe in Ruby



The unix | (pipe) operator is an extremely powerful and simple tool. With it you can do amazing feats, like kill vagrant when it's misbehaving:

1
ps aux | grep vagrant | grep -v grep | tr -s " " | cut -d" " -f2 | xargs kill

or open every file that references ShimMessageView:

1
vi $(ag ShimMessageView -l --nocolor | xargs)

Simply stated, the pipe takes input from the left, runs it through a command and outputs it on the right. Data in, data out. This pattern is so powerful, many languages, such as Clojure and Elixir, implement the pipe natively. Unfortunately Ruby does not.

While building our next generation API, I was searching for an way to make controller actions more transparent. Traditionally they're are a mess of state. Data is passed around behind the scenes making it very hard to follow the lifecycle of a request. That's when I thought "what about pipe?" It's easy, and when implemented using a functional, stateless style, should make our actions easy to follow. With that idea, the following method was born.

1
2
3
4
5
def pipe(response, through:)
  through.reduce(response) { |resp, method|
    resp.success? ? send(method, resp) : resp
  }.to_ary
end

Given a response object, pipe calls each method in the through array. If the result of the method is not successful (i.e. not a 2xx) then we break the pipe otherwise continue.

This allows us to write code like the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MemberApp < BaseApp
  get "/members/:id" do |id|
    collection = MemberMapper.new(identity_map).find_by_ids(id)
    response = Response.new(:collection => collection)

    pipe(response, :through => [
      :not_found, :authorize_to_read, :apply_writable, :decorate, :serialize
    ])
  end

  post "/members" do
    pipe(default_response, :through => [
      :build, :validate, :authorize_to_write, :create, :apply_writable,
      :decorate, :serialize
    ])
  end
end

class BaseApp < Sinatra::Base
  def not_found(response)
    if response.empty_collection?
      response.with(:status => 404)
    else
      response
    end
  end
end

When is the last time you were able to look at a controller method in a Rails app and know exactly what each step was? I haven't been able to since the first commit after rails new.

I'm sure this article brings up some questions too. What's this Response object? How is the result of pipe translated into something Rack understands? What's an identity_map? I'll explain these concepts and more in future articles.

comments powered by Disqus