defmodule HookSniff do
@moduledoc """
Official Elixir client for the HookSniff webhook delivery service.
## Usage
client = HookSniff.new("hr_live_...")
# Create endpoint
{:ok, endpoint} = HookSniff.Endpoints.create(client, %{url: "https://myapp.com/webhook"})
# Send webhook
{:ok, delivery} = HookSniff.Webhooks.send(client, %{
endpoint_id: endpoint["id"],
event: "order.created",
data: %{order_id: "12345"}
})
"""
@default_base_url "https://hooksniff-api-1046140057667.europe-west1.run.app/v1"
@default_timeout 30_000
@user_agent "hooksniff-elixir/0.2.0"
defstruct [:api_key, :base_url, :timeout]
@type t :: %__MODULE__{
api_key: String.t(),
base_url: String.t(),
timeout: pos_integer()
}
@doc """
Create a new HookSniff client.
"""
@spec new(String.t(), keyword()) :: t()
def new(api_key, opts \\ []) do
%__MODULE__{
api_key: api_key,
base_url: Keyword.get(opts, :base_url, @default_base_url),
timeout: Keyword.get(opts, :timeout, @default_timeout)
}
end
@doc false
def request(%__MODULE__{} = client, method, path, body \\ nil) do
url = client.base_url <> path
url_charlist = String.to_charlist(url)
headers = [
{'authorization', String.to_charlist("Bearer " <> client.api_key)},
{'content-type', 'application/json'},
{'user-agent', String.to_charlist(@user_agent)}
]
http_opts = [
timeout: client.timeout,
connect_timeout: client.timeout,
ssl: [verify: :verify_peer]
]
{content_type, req_body} =
case body do
nil -> {'application/json', []}
_ -> {'application/json', Jason.encode!(body)}
end
request = {url_charlist, headers, content_type, req_body}
case :httpc.request(method_charlist(method), request, http_opts, []) do
{:ok, {{_http_ver, status, _reason}, resp_headers, resp_body}} ->
handle_response(status, resp_headers, to_string(resp_body))
{:error, reason} ->
{:error, %HookSniff.Error{message: "HTTP error: #{inspect(reason)}", code: :network_error}}
end
end
defp method_charlist(:get), do: :get
defp method_charlist(:post), do: :post
defp method_charlist(:put), do: :put
defp method_charlist(:delete), do: :delete
defp handle_response(status, _headers, body) when status in 200..299 do
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:ok, body}
end
end
defp handle_response(400, _headers, body), do: {:error, parse_error(body, :validation_error)}
defp handle_response(401, _headers, body), do: {:error, parse_error(body, :authentication_error)}
defp handle_response(404, _headers, body), do: {:error, parse_error(body, :not_found_error)}
defp handle_response(413, _headers, body), do: {:error, parse_error(body, :payload_too_large_error)}
defp handle_response(429, _headers, body), do: {:error, parse_error(body, :rate_limit_error)}
defp handle_response(status, _headers, body), do: {:error, parse_error(body, :unknown_error, status)}
defp parse_error(body, code, status \\ nil) do
message =
case Jason.decode(body) do
{:ok, %{"error" => %{"message" => msg}}} -> msg
_ -> "HTTP #{status || code}"
end
%HookSniff.Error{message: message, code: code, status: status}
end
end