Callbacks and Ruby

Recently I experimented with idea of callbacks and Ruby. Some ideas are taken from Node.js, with which I had to work with for few months, and recently I saw talk by Jim Weirich on Decoupling from Rails, that inspired me to try callback approach in Ruby. The main goal of me dropping in to the callback land is decoupling from rails.
There has been a lot of talking lately about Rails and testing and I prefer to use TDD and draw clear boundaries in my code where it is possible.

When I have an concept in mind, I like to construct it's interface first. Sort of pseudo code with
actual Ruby code. What I want is to construct a class that can charge credit card and handle success and failure like so:

Payments::ChargeCreditCard.new(credit_card_params, payment_options).run(
  on_success: -> (response) {
    response.amount
    response.transaction_id
  },
  on_failure: -> (response) {
    response.failure_message
  }
)

So first thing here to notice is that I can do anything I want with response in callbacks and
I can convert this example to test just by doing this:

on_success: -> (response) {
  expect(response.amount).to eq(invoice.total_price)
  expect(response.transaction_id).to be_present
}

Same goes for failure. On happy path tests I intentionally raise an error in failure callback and vice versa.If something is not working correctly I get notified instantly:

  on_failure: -> (response) {
    raise "It should not get here: #{response.failure_message}"
  }

Let's go ahead and implement this:

module Payments
  class ChargeCreditCard
    class MissingCallbackError < KeyError;end
    class BadResponseError < KeyError;end

    def initialize(charge_options, payment_gateway)
      @charge_options, @payment_gateway = charge_options, payment_gateway
    end

    private

    attr_reader :charge_options,  :payment_gateway

    def run(callbacks)
      success_cb = callbacks.fetch(:on_success) {
        raise MissingCallbackError.new("Please provide success callback")
      }
      failure_cb = callbacks.fetch(:on_failure) {
        raise MissingCallbackError.new("Please provide failure callback")
      }
      gateway_response = payment_gateway.charge(charge_options)
      if gateway_response.success?
        response = Response.new
        response.amount = gateway_response.params.fetch(:amount) { |k|
          handle_bad_response(k) 
        }
        response.transaction_id = gateway_response.params.fetch(:transaction_id) { |k| 
          handle_bad_response(k) 
        }
        success_cb.call(response)
      else
        response = Response.new
        response.failture_message = gateway_response.prams.fetch(:error_msg) {|k| 
          handle_bad_response(k) 
        }
        failure_cb.call(response)
      end
    end

    def handle_bad_response(key)
      BadResponseError.new("#{key} is missing in the rsponse")
    end

    class Response
      attr_accessor :failure_message, :amount, :transaction_id
    end
  end
end

First thing to note here that this code forces you to handle two possible scenarios. You can't avoid that. Another thing is that
I am in control of the response and types of messages it can respond to. We can easily swap provider without changing the interface of
this class. Notice that I use fetch and custom errors here. I like to be aware if my assumptions about 3rd party response are not correct
and I like to know it early. I would catch this somewhere and report to Airbrake or similar tool. And possibly revert the transaction.
But that's just a detail.

We can go further and abstract the payment gateway with our own class, but that's out of the scope of this post. This code
has some rough spots, but I think it demonstrates the idea.

Janis Miezitis

Read more posts by this author.

Subscribe to Janis Miezitis personal blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!