defmodule Consul.Connection do
@moduledoc """
Handle Tesla connections for Consul.
"""
@type t :: Tesla.Env.client()
@default_scheme "http://"
@consulex_version Mix.Project.config() |> Keyword.get(:version, "")
@retry_defaults [
delay: 50,
max_retries: 5,
max_delay: 4_000,
should_retry: &Consul.Connection.match_errors/1
]
@doc """
Builds a base URL based on a given server spec.
"""
def base_url(<<"http://", _::binary>> = server_spec),
do: server_spec
def base_url(<<"https://", _::binary>> = server_spec),
do: server_spec
def base_url(server_spec) do
if Regex.match?(~r{^[^:/]+(:[0-9]+)?}, server_spec) do
@default_scheme <> server_spec
else
raise ArgumentError,
"expected :server_spec to be a valid URL or server spec, got: #{inspect(server_spec)}"
end
end
@doc """
Builds a Tesla client.
## Options
* `adapter` - specify custom Tesla adapter, if not set will use Tesla's
default one
* `retry` - options for `Tesla.Middleware.Retry` middleware
* `timeout` - when given, will include `Tesla.Middleware.Timeout`
middleware configured with given value
* `token` - when given, will include `x-consul-token` header with given
value in the requests
* `wait` - when given, will include `Tesla.Middleware.ConsulWatch`
middleware configured with given value in milliseconds
When using `wait` option, make sure its value is larger than the timeout
used by the underlying adapter (specified in options or default one).
Otherwise, the request may be timing out before wait period passes.
Note that using `timeout` option is not always sufficient to achieve this
as some adapters (e.g. `Tesla.Adapter.Finch`) are not compatible with
`Tesla.Middleware.Timeout` middleware that is used when `timeout` option
is specified.
"""
def new(base_url, opts \\ []) do
middleware = [
{Tesla.Middleware.BaseUrl, base_url},
Tesla.Middleware.DecompressResponse,
Tesla.Middleware.FollowRedirects
]
adapter = Keyword.get(opts, :adapter)
opts =
Keyword.update(opts, :retry, @retry_defaults, fn retry ->
Keyword.merge(@retry_defaults, retry)
end)
middleware =
Enum.reduce(opts, middleware, fn opt, middleware ->
plug_middleware(opt, middleware)
end)
Tesla.client(middleware, adapter)
end
defp plug_middleware({:timeout, timeout}, middleware) do
middleware ++ [{Tesla.Middleware.Timeout, timeout: timeout}]
end
defp plug_middleware({:token, token}, middleware) do
middleware ++ [{Tesla.Middleware.Headers, [{"x-consul-token", token}]}]
end
defp plug_middleware({:wait, wait}, middleware) do
middleware ++ [{Tesla.Middleware.ConsulWatch, wait: wait}]
end
defp plug_middleware({:retry, retry}, middleware) do
middleware ++ [{Tesla.Middleware.Retry, retry}]
end
defp plug_middleware(_opt, middleware) do
middleware
end
@doc """
Converts a Consul.Request struct into a keyword list to send via
Tesla.
"""
@spec build_request(Consul.Request.t()) :: keyword()
def build_request(request) do
[url: request.url, method: request.method]
|> build_query(request.query)
|> build_headers(request.header)
|> build_body(request.body)
end
defp build_query(output, []), do: output
defp build_query(output, query_params) do
Keyword.put(output, :query, query_params)
end
defp build_headers(output, header_params) do
api_client =
Enum.join(
[
"elixir/#{System.version()}",
"consulex/#{@consulex_version}"
],
" "
)
headers = [{"x-api-client", api_client} | header_params]
Keyword.put(output, :headers, headers)
end
# If no body or file fields and the request is a POST, set an empty body
defp build_body(output, []) do
method = Keyword.fetch!(output, :method)
set_default_body(output, method)
end
defp build_body(output, body: main_body) do
Keyword.put(output, :body, main_body)
end
@required_body_methods [:post, :patch, :put, :delete]
defp set_default_body(output, method) when method in @required_body_methods do
Keyword.put(output, :body, "")
end
defp set_default_body(output, _) do
output
end
@doc """
Execute a request on this connection
## Returns
* `{:ok, Tesla.Env.t}` - If the call was successful
* `{:error, reason}` - If the call failed
"""
@spec execute(Tesla.Client.t(), Consul.Request.t()) :: {:ok, Tesla.Env.t()}
def execute(connection, request) do
request
|> build_request()
|> (&Tesla.request(connection, &1)).()
end
def match_errors({:ok, %{status: status}}) when status >= 400, do: true
def match_errors({:ok, _}), do: false
def match_errors({:error, _}), do: true
end