Last month, Shane Emmons did a post on implementing pipe in ruby. Check out his post here.
As Shane mentioned, the unix pipe operator is extremely powerful and simple to use / understand. The result of the first operation is passed as the argument for the next operation and so on. Last summer, shortly before I came onboard, Shane implemented our first version:
def pipe(initial_env, through:)
through.reduce(initial_env) do |env, method|
status, _, _ = env
success?(status) ? send(method, env) : env
end
end
As we progressed, changes were made and additional, slightly different, versions
of the pipe
method started creeping in.
Here they are as of the beginning of 2/16/2015:
def pipe(response, through:)
through.reduce(response) { |resp, method|
resp.success? ? send(method, resp) : resp
}.to_ary
end
def pipe(response, through:)
through.reduce(response) { |resp, method|
resp.success? ? send(method, resp) : resp
}
end
def pipe(message, through: [])
through.reduce(message) { |msg, method|
break unless msg.continue?
begin
send(method, msg)
rescue => e
handle_error(e, { method: method, message: msg })
end
}
end
Sure enough, as I started on my most recent task, I came across the need to
write another slightly different version of the method. The code duplication
got to me. Another problem that the whole team was encountering had to do with
debugging and tracing errors. Ruby is normally very good at providing detailed
information about what's going wrong. In this case, however, errors would occur
in one of the methods specified in the through array and the stack trace would
point back to the pipe method. We didn't know which method was failing or even
what arguments were passed to it. Many of us found ourselves adding
STDOUT.puts
in each method called and debugging from there. For me at least,
this brought back nightmarish memories of alert debugging JavaScript prior to
the days of Firebug and Chrome's JS console.
In an attempt to rid myself of these nightmares, the pipe-ruby
gem was born.
As I looked at our implementations, there were a number of requirements popped out at me.
Additionally, I found that I would be iterating over an array and passing each
value to pipe
. It seemed easiest to add that to the GEM, so pipe_each
was
born.
The Pipe::Config
object provides the fine tuning capabilities defined by our
existing requirements.
Pipe::Config.new(
:error_handlers => [], # an array of procs to be called when an error occurs
:raise_on_error => true, # tells Pipe to re-raise errors which occur
:skip_on => false, # a truthy value or proc which tells pipe to skip the
# next method in the `through` array
:stop_on => false # a truthy value or proc which tells pipe to stop
# processing and return the current value
)
There are a number of ways to send a custom config to #pipe
or #pipe_each
.
They are fully described in the
README's Configurable Options section.
The README also does a great job of outlining
error handling and
skipping / stopping execution.