defmodule Plaid.Client do
@moduledoc """
Make API calls to plaid and convert the responses from JSON -> well typed elixir structs.
To use a different HTTP client, create a new module like `MyApp.PlaidClient` which implements
`post/3` and implements the `@behaviour Plaid.Client` behaviour. The success response of those functions must return a `:body` key with a JSON string value
and a `:status_code` key with an integer HTTP status. For an example, see the `Plaid.Client.HTTPoison` module.
> For network errors where you don't get a body or status code, you may return an error tuple
> with any error value, but the error value is not currently utilized.
"""
require Logger
alias Plaid.Castable
@doc """
Callback to POST the data to the Plaid API.
Will be called with the full URL, payload, and headers. Simply take these values
execute the HTTP request.
> `headers` passed in will be a list of two item tuples where the first item is the header key
> and the second is the value. e.g. `[{"content-type", "application/json"}]`
## Examples
iex> post("https://production.plaid.com/categories/get", ~s<{"thing": "stuff"}>, [{"content-type", "application/json"}])
{:ok, %{body: ~s<{"foo": "bar"}>, status_code: 200}}
"""
@callback post(url :: String.t(), payload :: String.t(), headers :: [{String.t(), String.t()}]) ::
{:ok, %{body: String.t(), status_code: integer()}} | {:error, any()}
@doc """
Make a Plaid API call.
Takes in everything needed to complete the request and
return a well formed struct of the response.
## Examples
call(
"/categories/get",
%{},
Plaid.Categories.GetResponse,
client_id: "123",
secret: "abc"
)
{:ok, %Plaid.Categories.GetResponse{}}
"""
@spec call(String.t(), map(), module(), Plaid.config()) ::
{:ok, any()} | {:error, Plaid.Error.t()}
def call(endpoint, payload \\ %{}, castable_module, config) do
url = build_url(config, endpoint)
payload =
payload
|> add_auth(config)
|> Jason.encode!()
headers = [{"content-type", "application/json"}]
http_client = Keyword.get(config, :http_client, Plaid.Client.HTTPoison)
case http_client.post(url, payload, headers) do
{:ok, %{body: body, status_code: status_code}} when status_code in 200..299 ->
{:ok, cast_body(body, castable_module)}
{:ok, %{body: body}} ->
{:error, cast_body(body, Plaid.Error)}
{:error, _error} ->
{:error, Castable.cast(Plaid.Error, %{})}
end
end
@spec build_url(Plaid.config(), String.t()) :: String.t()
defp build_url(config, endpoint) do
test_api_host = Keyword.get(config, :test_api_host)
if is_binary(test_api_host) do
test_api_host <> endpoint
else
env = Keyword.get(config, :env, :sandbox)
"https://#{env}.plaid.com#{endpoint}"
end
end
@spec add_auth(map(), Plaid.config()) :: map()
defp add_auth(payload, config) do
auth =
config
|> Map.new()
|> Map.take([:client_id, :secret])
Map.merge(payload, auth)
end
@spec cast_body(String.t(), module() | :raw) :: String.t() | %{optional(any) => any}
defp cast_body(body, :raw) do
body
end
defp cast_body(json_body, castable_module) do
case Jason.decode(json_body) do
{:ok, generic_map} -> Castable.cast(castable_module, generic_map)
_ -> Castable.cast(Plaid.Error, %{})
end
end
end