Easyhooks - Webhooks made easy
Easyhooks is a ruby gem created to handle webhooks for Rails ActiveRecord instances. Simple, easy and fast. You can use it to create webhooks for your models, and then use them to send data to your clients.
Requirements
- Ruby 3.0 or newer
- Rails 6.1 or newer (including Rails 7.0)
Installation
Include the gem in your Gemfile and run bundle to install it:
gem 'easyhooks'This step is not required if you don't want to store your hooks configuration in the database, but it's recommended.
rails g easyhooks:migration
rails db:migrateUsage
Defining the easiest hook
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com'
end
end
endThe example above it's the simplest use. It will create a trigger called approved for the User model. Whenever a user is created, updated or deleted,
the trigger approved dispatches an ActiveJob called PostProcessor to asynchronously send a POST request to the endpoint https://example.com with the following payload:
{
"object": "User",
"action": "my_first_action",
"trigger": {
"name": "approved",
"event": "CREATE"
},
"data": {
"id": 1
}
}Easy, no? Let's understand how everything works and see how to customize it even more.
Trigger
A trigger is a way to define when a webhook should be dispatched. It can be defined by the following options:
-
:on- Defines the events that will trigger the webhook. It can be:create,:updateor:destroy. Defaults to[:create, :update, :destroy]. -
:only- Defines the attributes that will trigger the webhook. It can be a single attribute or an array of attributes. Defaults tonil(or any model changes). Example:only: :nameoronly: [:name, :email].
Note: :only works only for :update events.
Example:
class User < ActiveRecord::Base
easyhooks do
trigger :approved, on: :update, only: :name do
action :my_first_action, endpoint: 'https://example.com'
end
end
endWhenever in your codebase a User is updated and the name attribute is changed, the trigger approved will dispatch the action my_first_action.
Action
An action is a way to define what should be done when a webhook is dispatched. It can be defined by the following options:
-
:endpoint- Defines the endpoint that will receive the webhook data. It must be a valid URL. -
:method- Defines the HTTP method that will be used to send the webhook. It can be:get,:post,:put,:patchor:delete. Defaults to:post. -
:headers- Defines the headers that will be sent with the webhook. It must be a hash. Defaults to{ 'Content-Type': 'application/json' }. -
:auth- Defines the authentication that will be used to send with the webhookAuthorizationheader. Is must be a string. Defaults tonil. Example:Basic YWRtaW46cGFzc3dvcmQ=.
Example:
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com', method: :put, headers: { 'X-Easy': 'Easyhooks' }, auth: 'Basic YWRtaW46cGFzc3dvcmQ='
end
end
endYou can also define multiple actions for a single trigger:
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com/first', method: put
action :my_second_action, endpoint: 'https://example.com/second', method: post
end
end
endCustomizing the Payload
The payload is the data that will be sent to the endpoint. It can be defined by the following options in any easyhooks block like trigger, action or even easyhooks:
-
:payload- Defines the payload that will be sent to the endpoint. It must be a symbol or a proc. Defaults to{ id: model.id }.
Note: If you define a payload in a trigger block, it will be used for all actions. If you define a payload in an action block, it will be used only for that action.
Example:
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com', payload: :my_payload
end
end
def my_payload
{ id: id, name: name }
end
endJSON Payload:
{
"object": "User",
"action": "my_first_action",
"trigger": {
"name": "approved",
"event": "CREATE"
},
"data": {
"id": 1,
"name": "John Doe"
}
}Adding conditions
You can add conditions to your triggers and actions. It can be defined by the following options:
-
:if- Defines a condition that will be evaluated before dispatching the webhook. It must be a symbol or a proc. Defaults tonil.
Note: If you define a condition in a trigger block, it will be used for all actions. If you define a condition in an action block, it will be used only for that action.
Example:
class User < ActiveRecord::Base
easyhooks do
trigger :approved, if: :my_condition do
action :my_first_action, endpoint: 'https://example.com'
end
end
def my_condition
name == 'John Doe'
end
endAccessing the webhook response data
You can access the webhook response data in your codebase. This will be useful if you want to do something with the response, like logging it.
Note: This callback will be called only if the webhook is successfully sent. Meaning that, if any error occurs while evaluating the webhook, this callback will not be called.
For failure callbacks, you can use the :on_fail option.
Note 2: The response object is an instance of Net::HTTPResponse.
Example:
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com' do |response|
puts response.code
puts response.body
end
end
end
endHandling webhook failures
You can handle webhook failures in your codebase. This will be useful let's say if the endpoint is down and you want to retry the webhook later.
You can define a :on_fail callback (symbol or proc) in any easyhooks block like trigger or action:
class User < ActiveRecord::Base
easyhooks do
trigger :approved do
action :my_first_action, endpoint: 'https://example.com', on_fail: :my_callback
end
end
def my_callback
# Do something
end
endGlobal configuration
Defining endpoints, headers and auth for each action can be a little bit annoying. You can define a global configuration for all actions in your codebase. There is three ways to do that:
- Using the
easyhooksblock - Using an YAML file
- Using the database
Using the easyhooks block
You can define a global configuration for all actions in your codebase using the easyhooks block:
class User < ActiveRecord::Base
easyhooks endpoint: 'https://example.com', auth: 'Bearer token' do
trigger :approved do
action :my_first_action, if: :my_condition
action :my_second_action, if: :my_second_condition
end
end
endNote: Easyhooks prioritizes the configuration defined in the action block over the configuration defined in the easyhooks block:
- Order of priority:
action>trigger>easyhooks>yaml>database.
Example:
class User < ActiveRecord::Base
easyhooks endpoint: 'https://example.com' do
trigger :approved do
action :my_first_action, method: :put
action :my_second_action
end
end
endIn the example above, the my_first_action will be sent using the PUT method, while the my_second_action will be sent using the POST method.
You can combine any number of configurations in your codebase and Easyhooks handle.
Using an YAML file
You can define a global configuration for all actions in your codebase using an YAML file:
# config/easyhooks.yml
development:
classes:
User:
endpoint: 'https://example.com'
method: :post
auth: 'Bearer token'
headers:
X-Easy: Easyhooks
triggers:
approved:
endpoint: 'https://example.com'
method: :patch
actions:
my_first_action:
endpoint: 'https://example.com'
method: :putIn the example above, we start configuring the hooks by environment.
An action should have a unique name and can be shared between classes. Same for triggers.
A class can have multiple trigger/actions and you can define a single configuration by class.
Note: The priority of the configurations defined in the YAML file is the same as mentioned before:
- Order of priority:
action>trigger>easyhooks>yaml>database.
Using the database (Stored configuration)
You can define a global configuration for everything in your codebase using the database. For that you will need to execute the migration generator and run the migration:
rails g easyhooks:migration
rails db:migrateDefine your models and hooks, but make sure to use the :stored option in the easyhooks block:
class User < ActiveRecord::Base
easyhooks :stored do
trigger :approved do
action :my_first_action
end
end
endThen, store the configuration in the database using the Easyhooks::Store model. Open the rails console and run:
stored_action = Easyhooks::Store.create!(context: 'actions', name: 'my_first_action', endpoint: 'https://example.com', method: :put)
stored_action.add_headers({ 'X-Easy': 'Easyhooks' })
stored_action.add_auth('Bearer', 'token')Using the database store will allow you to change the configuration without the need to restart your application, which is pretty useful, let`s say, if you want to change the endpoint of a webhook that is broken, or the auth token expired.
The context attribute can be actions, triggers or classes.
The name attribute is the name of the action, trigger or class.
Here you can also override the configurations using the priority order mentioned before:
class User < ActiveRecord::Base
easyhooks :stored do
trigger :approved do
action :my_first_action
action :my_second_action, method: :patch
end
end
endYou can also use the type :stored for blocks like trigger and action, and combine multiple rules:
Easyhooks::Store.create!(context: 'triggers', name: 'approved', method: :patch, endpoint: 'https://example.com/users')class User < ActiveRecord::Base
easyhooks do
trigger :approved, type: :stored do
action :my_first_action, payload: :my_payload
action :another_action, method: :post
end
trigger :deleted, on: :destroy, payload: :my_other_payload, if: :condition do
action :my_second_action, endpoint: 'https://example.com/users/deleted'
end
end
endConclusion
You can combine all the options mentioned above to create your own webhooks. Easyhooks is flexible and easy to use. Be creative and have fun!
Contributing
Bug reports and pull requests are welcome. This project is intended to be a safe, welcoming space for collaboration.
Future improvements
- Add option to temporarily disable a trigger, action or class hook
- Add option to retry a webhook if it fails
- Add option to define a timeout for the webhook
- Rails generator to create database stored hooks
- Rails generator to create YAML stored hooks
License
Apache License, Version 2.0. See LICENSE for details.
Copyright (c) 2023-2023 Thiago Bonfante