A recent issue of Rails Magazine published an article by John Nunemaker on web hooks in rails, along with a concise example for triggering hooks on models using an observer class. I’ve followed this lead, but found a few basic changes necessary to make the solution more tractable for use in a typical Rails API setup.
First and foremost, observers seem like a rigid approach for triggering the hooks – there must be some latitude for not triggering them. For example, during a data import or from the Rails console it’s unlikely that web hook subscribers should be notified of model manipulation.
The approach I took assumes a subscriber model. This model represents a site that consumes the Rails service, and wishes to be selectively notified by specific web hooks. Here’s a simple migration:
create_table :subscribers, :force => true do |t|
t.string ::name
end
create_table :web_hooks, force => true do |t|
t.integer :subscriber_id
t.string :callback_url
t.string :model
t.string :action
end
I won’t belabor the nominal subscriber model, but my web hook model is designed as follows, with a singleton method trigger_for_action encapsulating the bulk of the web hook logic.
require 'net/http'
class WebHook < ActiveRecord::Base
belongs_to :subscriber
class << self
def trigger_for_action(model,action,package)
hooks = WebHook.find_all_by_model_and_action(model,action)
hooks.each do |hook|
uri = URI.parse(hook.callback_url)
Net::HTTP.post_form(uri, {'data'=> package})
end
end
end
end
Building web hook notifications on Rails, it’s likely that the hooks will be working in concert with a public API. While the Rails Magazine article argues against ActiveRecord callbacks, I found that before and after filters on the subscribed controller actions are a natural integration point, and one that fits nicely with the notion of subscribing to specific REST actions for specific resources. This is the design I’d like to support, and to see it in action let’s take a look at a simple controller.
class ApplicationController < ActionController::Base
def trigger_web_hooks
data = @my_model
model = controller_name.downcase.singularize
action = action_name
WebHook.trigger_for_action(model,action,data)
end
end
class WidgetsController < ApplicationController
include ApplicationHelper
after_filter :trigger_web_hooks, :only => [:create, :update]
end
The only customization now will be to add the filters to the web hook integration points, and ensure that there’s always an instance variable available by the name @my_model so that it can be included in the callback body. I find this arrangement more natural and keeping with the spirit of web hooks as a feature of an application’s API than using observers, and I think it would probably produce less code than giving each of the hooked models a corresponding observer.
