Skip to content

Instantly share code, notes, and snippets.

@benedikt
Created September 12, 2017 13:56
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save benedikt/fa3488d968385e553884f61676a69f13 to your computer and use it in GitHub Desktop.
Save benedikt/fa3488d968385e553884f61676a69f13 to your computer and use it in GitHub Desktop.
# 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

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

@rebase-master
Copy link

Real helpful! Thank you for the effort. 👍

@ddlogesh
Copy link

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

@benedikt
Copy link
Author

@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