Wayne Conrad's Blog

home

Tiny DSL using throw/catch

14 Jul 2015

Someone asked me whether I had used Ruby’s throw/catch mechanism in production. I thought I had, but couldn’t remember a specific instance. Today, I ran across some of my production code that uses throw/catch.

The product has a form that the end user fills out in order to pay a bill. For the user’s convenience, the form is prefilled from any of several sources. For example, if the user has made a payment before, we’ll use the address they filled in last time. If they have not made a payment but the system knows the address that their statement was mailed to, it will use that. There is also a test mode that can provide prefills, and defaults to use when no prefill value is found.

The class that knows the prefill rules has a constructor that takes the different objects that prefill information can come from:

class BillPayPrefill

  def initialize(document, demo, merchant, bill_pay_request)
    @document = document
    @demo = demo
    @merchant = merchant
    @bill_pay_request = bill_pay_request
  end

  ...

end

There are about 20 fields, and so 20 methods to encapsulate the business rules for how prefill works with each field. At first, these methods looked like this:

def first_name
  if @bill_pay_request
    @bill_pay_request.billing_first_name
  elsif @document
    @document.first_name
  elsif test_mode?
    'BARNEY'
  else
    nil
  end
end

Just one or two methods like that is no big deal. But with 20 of them, we wanted a way to make the business rules stand out better. We ended up with this instead:

def first_name
  get_value do
    from_request :billing_first_name
    from_document :first_name
    when_test_mode 'BARNEY'
    default nil
  end
end

The DSL that makes this work is implemented in just a few private methods. This is the first of them. All it does is to catch a symbol and then yield to the passed block:

def get_value
  catch(:value) do
    yield
  end
end

The other methods throw a value, if it exists. If the value does not exist, they just return so that the next method can be tried:

def from_request(request_attribute)
  if @bill_pay_request
    throw :value, @bill_pay_request.send(request_attribute)
  end
end

def from_document(document_attribute)
  if @document
    throw :value, @document.send(document_attribute)
  end
end

def when_test_mode(test_value)
  if test_mode?
    throw :value, test_value
  end
end

def default(default_value)
  throw :value, default_value
end

The result of the top-level catch is the result of the first throw that gets executed. Using throw/catch gives the DSL a nice way to stop looking when the prefill value is found.

comments powered by Disqus