Skip to content

camirmas/grapple

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Grapple

🍏 Webhook magic in Elixir

Build Status

Grapple defines a simple API for hookable actions that broadcast updates to subscribers over HTTP.

This API lends itself nicely to Webhooks, REST Hooks, Server Push, and more!

Installation

  1. Add grapple to your list of dependencies in mix.exs:
def deps do
  [{:grapple, "~> 1.2.3"}]
end
  1. Ensure grapple is started before your application:
def application do
  [applications: [:grapple]]
end

Running

iex -S mix

Documentation

https://hexdocs.pm/grapple/1.2.3


Usage

Direct

The default struct, %Grapple.Hook{}, has the following fields:

  • url
  • owner
  • life
  • ref
  • method
  • body
  • headers
  • options
  • query

Note that url is required. Additionally, the fields body, headers, and options all correspond to those used in HTTPoison requests. See HTTPoison for more info.

Topics

To create a new topic, pass an atom to the add_topic function, which returns a Topic struct.

{:ok, topic = %Grapple.Server.Topic{}} = Grapple.add_topic(:pokemon)

Subscribing

To subscribe to a webhook, pass the topic name and a Hook to the subscribe function, which returns the topic name and the unique refernce to that particular hook:

{:ok, pid} = Grapple.subscribe(:pokemon, %Grapple.Hook{url: "my-api"})

It's important that topics are unique across your application's modules (and topicof ensures this) because it makes implementing higher-level features, such as REST Hooks, much easier.

Broadcasting

To broadcast all webhooks for a given topic, pass a topic name, and optionally arbitrary data. This will trigger HTTP requests for any stored hooks (and their subscribers) whose topic values match the given topic, and return the parsed responses.

# this will send hooks with their default `body`
Grapple.broadcast(:pokemon)

# you can also pass arbitrary data that will be sent instead
Grapple.Hook.broadcast("pokemon", data)

Note that the call to broadcast does not actually return the responses. This is because hooks run asynchronously. In order to retrieve responses, you can either ask for them explicitly:

[{_pid, responses}] = Grapple.get_responses(:pokemon)

Or you can, when subscribing a Hook, set :owner to the pid of an existing process that can receive a message when that Hook completes its broadcast. The format of the message is {:hook_response, hook_pid, response}, with response being a response from an HTTPoison request. See HTTPoison for more info. As an example, if your :owner process is a GenServer, you would define a handle_info function like so:

def handle_info({:hook_response, pid, response}, state) do
  # some logic
  {:noreply, state}
end

Polling

You can have individual hooks broadcast on an interval in two different ways. The first is to include an interval field with an integer (in milliseconds) when defining a Hook:

Grapple.subscribe(:pokemon, %Grapple.Hook{url: "my-api", interval: 3000}

You can also take an existing hook that does not yet have an interval, and tell it to start polling:

{:ok, pid} = Grapple.subscribe(:pokemon, %Grapple.Hook{url: "my-api", interval: 3000}
Grapple.start_polling(pid, 3000)

To turn off polling for a particular hook:

Grapple.stop_polling(pid)

To start polling for a particular hook, if it already has an interval:

Grapple.start_polling(pid)

Macro

Broadcasting can also be done via a macro, defhook. The macro defines a named method in the lexical module. When invoked, the method's name will be used as the topic, and if the method name matches an existing topic, all Hooks on that topic will be broadcast. The result will be broadcast as the body to any hook requests on that topic, unless it returns nil, in which case hooks will be sent with the default body.

The following example implements a hook that determines the game profile for Dragonite, automatically sending requests to the http://pokeapi.co:

Grapple.subscribe(:pokemon, %Grapple.Hook{url: "http://pokeapi.co/api/v2/pokemon/149"})

defmodule Pokemon do
  use Grapple.Hook

  defhook dragonite do
    # add some logic and return a body or return nil
    # In this case, sends "GET" request to the `Hook` URL.
  end
end

You should try to ensure that your hook method doesn't get called excessively since it's highly unlikely that subscribers will want to be repeatedly hit. This certainly depends on your own unique needs, but it's good to keep this fact in mind.

License

MIT