Current section
Files
Jump to
Current section
Files
lib/http.ex
defmodule Braintree.HTTP do
@moduledoc """
Base client for all server interaction, used by all endpoint specific
modules.
This request wrapper coordinates the remote server, headers, authorization
and SSL options.
Using `Braintree.HTTP` requires the presence of three config values:
* `merchant_id` - Braintree merchant id
* `private_key` - Braintree private key
* `public_key` - Braintree public key
All three values must be set or a `Braintree.ConfigError` will be raised at
runtime. All those config values support the `{:system, "VAR_NAME"}` as a
value - in which case the value will be read from the system environment with
`System.get_env("VAR_NAME")`.
"""
require Logger
alias Braintree.XML.{Decoder, Encoder}
alias Braintree.ErrorResponse, as: Error
@type response :: {:ok, Map.t | {:error, atom}} |
{:error, Error.t} |
{:error, binary}
@endpoints [
production: "https://api.braintreegateway.com/merchants/",
sandbox: "https://api.sandbox.braintreegateway.com/merchants/"
]
@cacertfile "/certs/api_braintreegateway_com.ca.crt"
@headers [
{"Accept", "application/xml"},
{"User-Agent", "Braintree Elixir/0.1"},
{"Accept-Encoding", "gzip"},
{"X-ApiVersion", "4"},
{"Content-Type", "application/xml"}
]
@statuses %{
400 => :bad_request,
401 => :unauthorized,
403 => :forbidden,
404 => :not_found,
426 => :upgrade_required,
500 => :server_error,
503 => :service_unavailable
}
@doc """
Centralized request handling function. All convenience structs use this
function to interact with the Braintree servers. This function can be used
directly to supplement missing functionality.
## Example
defmodule MyApp.Disbursement do
alias Braintree.HTTP
def disburse(params \\ %{}) do
HTTP.request(:get, "disbursements", params)
end
end
"""
@spec request(atom, binary, binary | Map.t, Keyword.t) :: response
def request(method, path, body \\ %{}, opts \\ []) do
response = :hackney.request(method, build_url(path, opts), build_headers(opts), encode_body(body), build_options())
case response do
{:ok, code, _headers, body} when code >= 200 and code <= 399 ->
{:ok, decode_body(body)}
{:ok, 422, _headers, body} ->
error =
body
|> decode_body()
|> Map.get("api_error_response")
|> Error.new()
{:error, error}
{:ok, code, _headers, _body} when code >= 400 and code <= 504 ->
{:error, code_to_reason(code)}
{:error, reason} ->
{:error, reason}
end
end
for method <- ~w(get delete post put)a do
@spec unquote(method)(binary) :: response
@spec unquote(method)(binary, map | list) :: response
@spec unquote(method)(binary, map, list) :: response
def unquote(method)(path) do
request(unquote(method), path, %{}, [])
end
def unquote(method)(path, payload) when is_map(payload) do
request(unquote(method), path, payload, [])
end
def unquote(method)(path, opts) when is_list(opts) do
request(unquote(method), path, %{}, opts)
end
def unquote(method)(path, payload, opts) do
request(unquote(method), path, payload, opts)
end
end
## Helper Functions
@doc false
@spec build_url(binary, Keyword.t) :: binary
def build_url(path, opts) do
environment = Keyword.get(opts, :environment, Braintree.get_env(:environment, :sandbox))
merchant_id = Keyword.get(opts, :merchant_id, Braintree.get_env(:merchant_id))
Keyword.fetch!(@endpoints, environment) <> merchant_id <> "/" <> path
end
@doc false
@spec encode_body(binary | Map.t) :: binary
def encode_body(body) when body == "" or body == %{}, do: ""
def encode_body(body), do: Encoder.dump(body)
@doc false
@spec decode_body(binary) :: Map.t
def decode_body(body) do
body
|> :zlib.gunzip()
|> String.trim()
|> Decoder.load()
rescue
ErlangError -> Logger.error("unprocessable response")
end
@doc false
@spec basic_auth(binary, binary) :: binary
def basic_auth(user, pass) do
"Basic " <> :base64.encode("#{user}:#{pass}")
end
@doc false
@spec build_headers(Keyword.t) :: [tuple]
def build_headers(opts) do
public = Keyword.get(opts, :public_key, Braintree.get_env(:public_key))
private = Keyword.get(opts, :private_key, Braintree.get_env(:private_key))
[{"Authorization", basic_auth(public, private)} | @headers]
end
@doc false
@spec build_options() :: [...]
def build_options do
cacertfile = Path.join(:code.priv_dir(:braintree), @cacertfile)
http_opts = Braintree.get_env(:http_options, [])
[:with_body, ssl_options: [cacertfile: cacertfile]] ++ http_opts
end
@doc false
@spec code_to_reason(integer | atom) :: integer
def code_to_reason(integer)
for {code, status} <- @statuses do
def code_to_reason(unquote(code)), do: unquote(status)
end
end