Skip to content

Instantly share code, notes, and snippets.

@joshmn
Forked from endymion/contact.rb
Created May 4, 2017 20:24
Show Gist options
  • Save joshmn/9f4139b58342d8ae41ece993302b2632 to your computer and use it in GitHub Desktop.
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.
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
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
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
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
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
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