Current section

Files

Jump to
alpaca_elixir lib alpaca client.ex
Raw

lib/alpaca/client.ex

defmodule Alpaca.Client do
@moduledoc """
Client responsible for making requests to Alpaca and handling the responses.
This client allows us to define all of our HTTP request logic and configuration
in one place so that we can easily change or adapt this away from all of the other
business logic of our library.
We have access to HTTP request types on this client and it will automatically use a base
url as well as set all the necessary headers for us.
We have access to GET, POST, PATCH, and DELETE HTTP requests using this client.
### Example usage
```
Alpaca.Client.get("/path", %{query_param: "value"})
Alpaca.Client.post("/path", %{param1: "value"})
Alpaca.Client.patch("/path", %{param1: "value"})
Alpaca.Client.delete("/path")
```
"""
defmodule MissingCredentialsError do
@moduledoc """
Exception for when a request is made without a Client ID and Secret.
"""
defexception message: """
The client_id and client_secret settings are required to make requests to Alpaca.
Please configure :client_id and :client_secret in config.exs or set the ALPACA_CLIENT_ID
and ALPACA_CLIENT_SECRET environment variable.
config :alpaca_elixir, client_id: CLIENT_ID, client_secret: CLIENT_SECRET
"""
end
@spec client_id(atom) :: String.t()
def client_id(env_key \\ :client_id) do
case Confex.get_env(:alpaca_elixir, env_key, System.get_env("ALPACA_CLIENT_ID")) || :not_found do
:not_found -> raise MissingCredentialsError
value -> value
end
end
@spec client_secret(atom()) :: String.t()
def client_secret(env_key \\ :client_secret) do
case Confex.get_env(:alpaca_elixir, env_key, System.get_env("ALPACA_CLIENT_SECRET")) ||
:not_found do
:not_found -> raise MissingCredentialsError
value -> value
end
end
@spec api_host :: String.t()
def api_host,
do: Application.get_env(:alpaca_elixir, :api_host, System.get_env("ALPACA_API_HOST"))
@spec data_api_host :: String.t()
def data_api_host,
do: "https://data.alpaca.markets"
@doc """
Issue a get request using the HTTP client
Accepts path which is the url path of the request, will be added to the end of the base_url
and a map of params which will become the query list for the get request
"""
@spec get(String.t(), map()) :: {:ok, map()} | {:error, map()}
def get(path, params \\ %{}, opts \\ []) do
opts
|> create()
|> Tesla.get(path, query: map_to_klist(params))
end
@doc """
Issue a post request using the HTTP client
Accepts path which is the url path of the request, will be added to the end of the base_url
and a map of params which will be the body of the post request
"""
@spec post(String.t(), map()) :: {:ok, map()} | {:error, map()}
def post(path, params \\ %{}, opts \\ []) do
opts
|> create()
|> Tesla.post(path, params)
end
@doc """
Issue a patch request using the HTTP client
Accepts path which is the url path of the request, will be added to the end of the base_url
and a map of params which will be the body of the post request
"""
@spec patch(String.t(), map()) :: {:ok, map()} | {:error, map()}
def patch(path, params \\ %{}, opts \\ []) do
opts
|> create()
|> Tesla.patch(path, params)
end
@doc """
Issue a put request using the HTTP client
Accepts path which is the url path of the request, will be added to the end of the base_url
and a map of params which will be the body of the post request
"""
@spec put(String.t(), map()) :: {:ok, map()} | {:error, map()}
def put(path, params \\ %{}, opts \\ []) do
opts
|> create()
|> Tesla.put(path, params)
end
@doc """
Issue a delete request using the HTTP client
Accepts path which is the url path of the request, will be added to the end of the base_url
"""
@spec delete(String.t()) :: {:ok, map()} | {:error, map()}
def delete(path, opts \\ []) do
opts
|> create()
|> Tesla.delete(path)
end
defp map_to_klist(dict) do
Enum.map(dict, fn {key, value} -> {to_atom(key), value} end)
end
defp to_atom(atom) when is_atom(atom), do: atom
defp to_atom(string), do: String.to_atom(string)
defp create(opts) do
api_host = Keyword.get(opts, :api_host, api_host())
additional_headers = Keyword.get(opts, :additional_headers, [])
headers = default_headers() ++ additional_headers
middleware = [
{Tesla.Middleware.BaseUrl, api_host},
{Tesla.Middleware.Headers, headers},
Tesla.Middleware.EncodeJson,
Alpaca.ResponseMiddleware
]
adapter = {Tesla.Adapter.Hackney, [recv_timeout: 30_000]}
Tesla.client(middleware, adapter)
end
@spec default_headers() :: %{String.t() => String.t()}
defp default_headers() do
[
{"APCA-API-KEY-ID", client_id()},
{"APCA-API-SECRET-KEY", client_secret()}
]
end
end