Skip to content

Instantly share code, notes, and snippets.

@joshmn joshmn/contact.rb forked from endymion/contact.rb
Created May 4, 2017

Embed
What would you like to do?
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 => 'xyz@venuedriver.com')
# 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 => 'xyz@venuedriver.com')
@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 => 'xyz@venuedriver.com')
@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
You can’t perform that action at this time.