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:
ps aux | grep vagrant | grep -v grep | tr -s " " | cut -d" " -f2 | xargs kill
or open every file that references ShimMessageView:
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.
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.
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.