-
-
Save joshmn/9f4139b58342d8ae41ece993302b2632 to your computer and use it in GitHub Desktop.
Example of integrating a Ruby on Rails app with Zapier using the REST hooks pattern. With support for triggering the REST hooks from Resque background jobs.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Contact < ActiveRecord::Base | |
... | |
def after_create | |
if Hook.hooks_exist?('new_contact', self) | |
Resque.enqueue(Hook, self.class.name, self.id) | |
# To trigger directly without Resque: Hook.trigger('new_contact', self) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ContactTest < ActiveSupport::TestCase | |
... | |
require 'fakeweb' | |
def test_trigger_rest_hook_on_contact_creation | |
account = Factory(:account) | |
subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
hook = Hook.create( | |
{ | |
"event" => "new_contact", | |
"account_id" => account.id, | |
"subscription_url" => subscription_url, | |
"target_url" => target_url | |
} | |
) | |
FakeWeb.register_uri( | |
:post, | |
target_url, | |
:body => 'irrelevant', | |
:status => ['200', 'Triggered'] | |
) | |
contact = Factory(:contact, :account => account, | |
:first => 'Ryan', :last => 'Porter', :email => '[email protected]') | |
# Simulate a Resque worker. | |
Hook.perform('Contact', contact.id) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(contact), FakeWeb.last_request.body | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'resque-retry' | |
class Hook < ActiveRecord::Base | |
attr_accessible :event, :account_id, :subscription_url, :target_url | |
validates_presence_of :event, :account_id, :subscription_url, :target_url | |
# Looks for an appropriate REST hook that matches the record, and triggers the hook if one exists. | |
def self.trigger(event, record) | |
hooks = self.hooks(event, record) | |
return if hooks.empty? | |
unless Rails.env.development? | |
# Trigger each hook if there is more than one for an account, which can happen. | |
hooks.each do |hook| | |
# These use puts instead of Rails.logger.info because this happens from a Resque worker. | |
puts "Triggering REST hook: #{hook.inspect}" | |
puts "REST hook event: #{event}" | |
encoded_record = HookEncoder.encode(record) | |
puts "REST hook record: #{encoded_record}" | |
RestClient.post(hook.target_url, encoded_record) do |response, request, result| | |
if response.code.eql? 410 | |
puts "Destroying REST hook because of 410 response: #{hook.inspect}" | |
hook.destroy | |
end | |
end | |
end | |
end | |
end | |
# This method is called by a Resque worker. Resque stores the record's class and ID, and the | |
# Resque worker provides those values as parameters to this method. | |
def self.perform(klass, id) | |
# puts "Performing REST hook Resque job: #{klass} #{id}" | |
event = "new_#{klass.to_s.underscore}" | |
record = klass.camelize.constantize.find(id) | |
Hook.trigger(event, record) | |
end | |
@queue = :rest_hook | |
extend Resque::Plugins::Retry | |
@retry_limit = 3 | |
@retry_delay = 5 | |
# Returns all hooks for a given event and account. | |
def self.hooks(event, record) | |
Hook.find(:all, :conditions => { | |
:event => event, | |
:account_id => record.account_id, | |
}) | |
end | |
# Tests whether any hooks exist for a given event and account, for deciding whether or not to | |
# enqueue Resque jobs. | |
def self.hooks_exist?(event, record) | |
self.hooks(event, record).size > 0 | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test_helper' | |
require 'fakeweb' | |
class HookTest < ActiveSupport::TestCase | |
def setup | |
@account = Factory(:account) | |
@subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
@target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
# Create the record before the hook is created or else the after_create Active Model hook | |
# will trigger the REST hook prematurely during testing. | |
@contact = Factory(:contact, :account => @account, | |
:first => 'Ryan', :last => 'Porter', :email => '[email protected]') | |
@hook = Hook.create( | |
{ | |
"event" => "new_contact", | |
"account_id" => @account.id, | |
"subscription_url" => @subscription_url, | |
"target_url" => @target_url | |
} | |
) | |
end | |
def test_hooks_exist | |
assert_equal true, Hook.hooks_exist?('new_contact', @contact) | |
Hook.destroy_all | |
assert_equal false, Hook.hooks_exist?('new_contact', @contact) | |
end | |
def test_hooks | |
hooks = Hook.hooks('new_contact', @contact) | |
assert_equal 1, hooks.size | |
assert_equal @hook, hooks.first | |
second_hook = Hook.create( | |
{ | |
"event" => "new_contact", | |
"account_id" => @account.id, | |
"subscription_url" => @subscription_url, | |
"target_url" => @target_url | |
} | |
) | |
hooks = Hook.hooks('new_contact', @contact) | |
assert_equal 2, hooks.size | |
assert_equal second_hook, hooks.last | |
end | |
def test_trigger | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['200', 'Triggered'] | |
) | |
FakeWeb.allow_net_connect = false | |
Hook.trigger('new_contact', @contact) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 1, Hook.count # The hook should not have been deleted. | |
end | |
def test_trigger_remove_hook_on_410_response | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['410', 'Danger, Will Robinson!'] | |
) | |
FakeWeb.allow_net_connect = false | |
Hook.trigger('new_contact', @contact) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 0, Hook.count # The 410 response should trigger removal of the hook. | |
end | |
def test_resque_background_job | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['200', 'Triggered'] | |
) | |
FakeWeb.allow_net_connect = false | |
# A Resque worker will normally do this, which should have the same effect as when the hook | |
# is manually triggered in the test_trigger test. | |
Hook.perform(Contact.name, @contact.id) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 1, Hook.count # The hook should not have been deleted. | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class HooksController < ApplicationController | |
def create | |
hook = Hook.new params | |
render :nothing => true, :status => 500 and return unless hook.save | |
Rails.logger.info "Created REST hook: #{hook.inspect}" | |
# The Zapier documentation says to return 201 - Created. | |
render :json => hook.to_json(:only => :id), :status => 201 | |
end | |
def destroy | |
hook = Hook.find(params[:id]) if params[:id] | |
if hook.nil? && params[:subscription_url] | |
hook = Hook.find_by_subscription_url(params[:subscription_url]).destroy | |
end | |
Rails.logger.info "Destroying REST hook: #{hook.inspect}" | |
hook.destroy | |
render :nothing => true, :status => 200 | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test_helper' | |
require 'fakeweb' | |
class HookTest < ActiveSupport::TestCase | |
def setup | |
@account = Factory(:account) | |
@subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
@target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/" | |
# Create the record before the hook is created or else the after_create Active Model hook | |
# will trigger the REST hook prematurely during testing. | |
@contact = Factory(:contact, :account => @account, | |
:first => 'Ryan', :last => 'Porter', :email => '[email protected]') | |
@hook = Hook.create( | |
{ | |
"event" => "new_contact", | |
"account_id" => @account.id, | |
"subscription_url" => @subscription_url, | |
"target_url" => @target_url | |
} | |
) | |
end | |
def test_hooks_exist | |
assert_equal true, Hook.hooks_exist?('new_contact', @contact) | |
Hook.destroy_all | |
assert_equal false, Hook.hooks_exist?('new_contact', @contact) | |
end | |
def test_hooks | |
hooks = Hook.hooks('new_contact', @contact) | |
assert_equal 1, hooks.size | |
assert_equal @hook, hooks.first | |
second_hook = Hook.create( | |
{ | |
"event" => "new_contact", | |
"account_id" => @account.id, | |
"subscription_url" => @subscription_url, | |
"target_url" => @target_url | |
} | |
) | |
hooks = Hook.hooks('new_contact', @contact) | |
assert_equal 2, hooks.size | |
assert_equal second_hook, hooks.last | |
end | |
def test_trigger | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['200', 'Triggered'] | |
) | |
FakeWeb.allow_net_connect = false | |
Hook.trigger('new_contact', @contact) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 1, Hook.count # The hook should not have been deleted. | |
end | |
def test_trigger_remove_hook_on_410_response | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['410', 'Danger, Will Robinson!'] | |
) | |
FakeWeb.allow_net_connect = false | |
Hook.trigger('new_contact', @contact) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 0, Hook.count # The 410 response should trigger removal of the hook. | |
end | |
def test_resque_background_job | |
FakeWeb.register_uri( | |
:post, | |
@target_url, | |
:body => 'irrelevant', | |
:status => ['200', 'Triggered'] | |
) | |
FakeWeb.allow_net_connect = false | |
# A Resque worker will normally do this, which should have the same effect as when the hook | |
# is manually triggered in the test_trigger test. | |
Hook.perform(Contact.name, @contact.id) | |
assert_equal "POST", FakeWeb.last_request.method | |
assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body | |
assert_equal 1, Hook.count # The hook should not have been deleted. | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment