Current section
Files
Jump to
Current section
Files
lib/esi/request.ex
defmodule ESI.Request do
@enforce_keys [
:verb,
:path
]
defstruct [
:verb,
:path,
opts_schema: %{},
opts: %{}
]
@type t :: %__MODULE__{
verb: :get | :post | :put | :delete,
path: String.t(),
opts_schema: %{atom => {:body | :query, :required | :optional}},
opts: %{atom => any}
}
@typedoc """
Additional request options.
You can provide any options that the API accepts, and/or these common options:
- `datasource` -- (DEFAULT: :tranquility) — The server name you would like data from
- `user_agent` -- Client identifier
"""
@type request_opts :: [request_opt]
@type request_opt ::
{:datasource, :tranquility | :singularity} | {:user_agent, String.t()} | {atom, any}
@doc """
Add query options to a request
"""
@spec options(req :: ESI.Request.t(), opts :: request_opts) :: ESI.Request.t()
def options(req, []) do
req
end
def options(req, opts) do
%{req | opts: Map.merge(req.opts, Map.new(opts))}
end
@base "https://esi.evetech.net/latest"
@doc """
Run a request.
"""
@spec run(t) :: {:ok, any} | {:error, any}
def run(request) do
case validate(request) do
:ok ->
do_run(request)
other ->
other
end
end
@doc """
Validate that the request is ready.
"""
@spec validate(request :: t) :: :ok | {:error, String.t()}
def validate(request) do
Enum.reduce(request.opts_schema, [], fn
{key, {_, :required}}, acc ->
case Map.has_key?(request.opts, key) do
true ->
acc
false ->
[key | acc]
end
_, acc ->
acc
end)
|> case do
[] ->
:ok
[missing_one] ->
{:error, "missing option `#{inspect(missing_one)}`"}
missing_many ->
detail = Enum.map(missing_many, &"`#{inspect(&1)}`") |> Enum.join(", ")
{:error, "missing options #{detail}"}
end
end
defp do_run(request) do
encoded_opts = encode_options(request)
url = @base <> request.path <> encoded_opts.query
case :hackney.request(request.verb, url, [], encoded_opts.body, [
:with_body,
follow_redirect: true,
recv_timout: 30_000
]) do
{:ok, code, _headers, body} when code in 200..299 ->
Poison.decode(body)
{:ok, 404, _, body} ->
case Poison.decode(body) do
{:ok, %{"error" => eve_error}} ->
{:error, eve_error}
_ ->
{:error, "HTTP 404"}
end
{:ok, code, _, _} ->
{:error, "HTTP #{code}"}
{:error, :timeout} ->
{:error, "timeout"}
end
end
@spec opts_by_location(request :: t) :: %{(:body | :query) => %{atom => any}}
def opts_by_location(request) do
Enum.reduce(request.opts, %{body: %{}, query: %{}}, fn {key, value}, acc ->
case Map.get(request.opts_schema, key) do
{location, _} ->
update_in(acc, [location], &Map.put(&1, key, value))
_ ->
acc
end
end)
end
@spec encode_options(request :: t) :: %{(:body | :query) => String.t()}
def encode_options(request) do
opts = opts_by_location(request)
%{
body: encode_options(:body, opts.body),
query: encode_options(:query, opts.query)
}
end
@spec encode_options(:body | :query, opts :: map) :: String.t()
defp encode_options(:body, opts) when map_size(opts) == 0, do: ""
# In the body, only support one option and just encode the value
defp encode_options(:body, opts), do: Poison.encode!(opts |> Map.values() |> hd)
defp encode_options(:query, opts) when map_size(opts) == 0, do: ""
defp encode_options(:query, opts), do: "?" <> URI.encode_query(opts)
def stream!(%{opts_schema: %{page: _}} = request) do
request_fun = fn page ->
options(request, page: page)
|> run
end
first_page = Map.get(request.opts, :page, 1)
Stream.resource(
fn -> {request_fun, first_page} end,
fn
:quit ->
{:halt, nil}
{fun, page} ->
case fun.(page) do
{:ok, []} ->
{[], :quit}
{:ok, data} when is_list(data) ->
{data, {fun, page + 1}}
{:ok, data} ->
{[data], :quit}
{:error, err} ->
raise err
end
end,
& &1
)
end
def stream!(request) do
Stream.resource(
fn -> request end,
fn
:quit ->
{:halt, nil}
request ->
case run(request) do
{:ok, data} ->
{List.wrap(data), :quit}
{:error, err} ->
raise err
end
end,
& &1
)
end
end