Skip to content

Instantly share code, notes, and snippets.

@benedikt
Created Sep 12, 2017
Embed
What would you like to do?
# app/models/webhook/delivery.rb
module Webhook
module Delivery
extend ActiveSupport::Concern
def webhook_payload
{}
end
def webhook_scope
raise NotImplementedError
end
def deliver_webhook(action)
event_name = "#{self.class.name.underscore}_#{action}"
deliver_webhook_event(event_name, webhook_payload)
end
def deliver_webhook_event(event_name, payload)
event = Webhook::Event.new(event_name, payload || {})
webhook_scope.webhook_endpoints.for_event(event_name).each do |endpoint|
endpoint.deliver(event)
end
end
end
end
# app/workers/webhook/delivery_worker.rb
require 'net/http'
module Webhook
class DeliveryWorker
include Sidekiq::Worker
def perform(endpoint_id, payload)
return unless endpoint = Webhook::Endpoint.find(endpoint_id)
response = request(endpoint.target_url, payload)
case response.code
when 410
endpoint.destroy
when 400..599
raise response.to_s
end
end
private
def request(endpoint, payload)
uri = URI.parse(endpoint)
request = Net::HTTP::Post.new(uri.request_uri)
request['Content-Type'] = 'application/json'
request.body = payload
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.request(request)
end
end
end
# app/models/webhook/endpoint.rb
module Webhook
class Endpoint < ApplicationRecord
def self.table_name_prefix
'webhook_'
end
attribute :events, :string, array: true, default: []
belongs_to :account
validates :target_url,
presence: true,
format: URI.regexp(%w(http https))
validates :events,
presence: true
def self.for_event(events)
where('events @> ARRAY[?]::varchar[]', Array(events))
end
def events=(events)
events = Array(events).map { |event| event.to_s.underscore }
super(Webhook::Event::EVENT_TYPES & events)
end
def deliver(event)
Webhook::DeliveryWorker.perform_async(id, event.to_json)
end
end
end
# app/models/webhook/event.rb
module Webhook
class Event
EVENT_TYPES = %w(
project_created
project_updated
project_destroyed
).freeze
attr_reader :event_name, :payload
def initialize(event_name, payload = {})
@event_name = event_name
@payload = payload
end
def as_json(*args)
hash = payload.transform_values do |value|
serialize_resource(value).as_json(*args)
end
hash[:event_name] = event_name
hash
end
private
def serialize_resource(resource)
ActiveModelSerializers::SerializableResource.new(resource, {})
end
end
end
# app/models/webhook/observable.rb
module Webhook
module Observable
extend ActiveSupport::Concern
include Webhook::Delivery
included do
after_commit on: :create do
deliver_webhook(:created)
end
after_commit on: :update do
deliver_webhook(:updated)
end
after_commit on: :destroy do
deliver_webhook(:destroyed)
end
end
end
end
# app/models/project.rb
class Project < ApplicationRecord
include Webhook::Observable
belongs_to :account
private
def webhook_scope
account
end
def webhook_payload
{ project: self }
end
end
@mhenrixon
Copy link

mhenrixon commented Sep 26, 2017

Really nice solution! Thanks for sharing 👍

@zauzaj
Copy link

zauzaj commented Sep 29, 2017

Thanks for nice solution.
One thing:
webhook_scope.webhook_endpoints.for_event(event_name) -> event_name is passed as string while you're expecting list of events in method definition:

    def self.for_event(events)
      where('events @> ARRAY[?]::varchar[]', Array(events))
    end

@steady-daddy
Copy link

steady-daddy commented Nov 2, 2020

Real helpful! Thank you for the effort. 👍

@ddlogesh
Copy link

ddlogesh commented Apr 16, 2021

How to implement the same with version-based?
Api (v1) -> Webhook (v1)
Api (v2) -> Webhook (v2)

@benedikt
Copy link
Author

benedikt commented Apr 16, 2021

@ddlogesh You could add a api_version column to the webhook endpoint and dynamically switch the serializer based on that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment