Skip to content

Instantly share code, notes, and snippets.

@geofflane
Last active January 18, 2017 03:42
Show Gist options
  • Save geofflane/67aa78675a88effa2ca03fb096fc45ee to your computer and use it in GitHub Desktop.
Save geofflane/67aa78675a88effa2ca03fb096fc45ee to your computer and use it in GitHub Desktop.
HTTPoison Stateless API
@doc """
Struct to hold the configurable portions of HTTPoision
"""
defmodule HTTPoison.Client do
defstruct process_url: &__MODULE__.process_url/1,
process_request_body: &__MODULE__.process_request_body/1,
process_request_headers: &__MODULE__.process_request_headers/1,
process_request_options: &__MODULE__.process_request_options/1,
process_response_body: &__MODULE__.process_response_body/1,
process_headers: &__MODULE__.process_headers/1,
process_status_code: &__MODULE__.process_status_code/1
def process_url(url), do: url
def process_request_body(body), do: body
def process_request_headers(headers), do: headers
def process_request_options(opts), do: opts
def process_response_body(body), do: body
def process_headers(headers), do: headers
def process_status_code(status_code), do: status_code
end
@doc """
API that takes the client and applies the configuration
"""
defmodule HTTPoison.API do
def post(client, url, body, headers \\ [], options \\ []) do
request(client, :post, url, body, headers, options)
end
def get(client, url, headers \\ [], options \\ []) do
request(client, :get, url, "", headers, options)
end
def delete(client, url, headers \\ [], options \\ []) do
request(client, :delete, url, "", headers, options)
end
# Make the actual request
# This uses an undocumented function in HTTPoision which is probably a bit sketchy
defp request(client, method, url, body, headers, options) do
url =
if Keyword.has_key?(options, :params) do
url <> "?" <> URI.encode_query(options[:params])
else
url
end
url = client.process_url.(to_string(url))
body = client.process_request_body.(body)
headers = client.process_request_headers.(headers)
options = client.process_request_options.(options)
HTTPoison.Base.request(__MODULE__, method, url, body, headers, options, client.process_status_code, client.process_headers, client.process_response_body)
end
end
# How you would use it in your own code
@doc """
client = Example.Pulsar.client(:service)
HTTPosion.API.get(client, "/foo")
"""
defmodule Example.Pulsar do
require Logger
def client(key) do
%HTTPoison.Client{process_url: process_url(key),
process_request_body: &json_encode/1,
process_response_body: &json_decode/1,
process_request_headers: &ensure_json_header/1,
process_request_options: &increase_timeouts/1,
}
end
def process_url(key) do
endpoint = endpoint(key)
fn (url) ->
endpoint <> url
end
end
def json_encode(body) do
Logger.debug("Connection body: #{inspect body}")
body
|> Poison.encode!()
end
def json_decode(body) do
Logger.debug("Connection response: #{inspect body}")
case Poison.decode(body) do
{:ok, response} -> response
error -> error
end
end
def ensure_json_header(headers) do
Enum.into(headers, [{"Content-Type", "application/json"}])
end
def increase_timeouts(opts) do
opts
|> PffLive.Pulsar.default_options(:timeout, 10000)
|> PffLive.Pulsar.default_options(:recv_timeout, 30000)
end
defp config(key) do
Application.fetch_env!(:example, key)
end
defp endpoint(key) do
config(key)
|> Keyword.get(:pulsar_endpoint)
end
def default_options(options, keyword, default) do
val = Keyword.get(options, keyword)
if val do
options
else
[{keyword, default} | options]
end
end
end
@edgurgel
Copy link

There are multiple ways to achieve this with HTTPoison. You could redefine request/5 and receive the endpoint that you intend as an option. Then the "base url" will be defined at that stage.

I also experimented a bit with a Behaviour: https://github.com/edgurgel/httpehaviour

@geofflane
Copy link
Author

The behavior is interesting but doesn't seem to achieve what I want to do. Maybe a more generic idea:
How could someone create a generic JSON client that set the proper headers, transformed the body on the request and response, etc. but only change the endpoint so that they had an instance that they could use to call ServiceA and a different instance they could use to call ServiceB? That's the fundamental problem I was trying to solve. The only way I could see to do that was to turn the "client" into data instead of having it hardcoded in a module.

@edgurgel
Copy link

One other way is to pass an option which is always the last argument. Then build the url with the option passed?

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