Current section
Files
Jump to
Current section
Files
lib/api_brasil/core/config.ex
defmodule ApiBrasil.Core.Config do
@moduledoc """
Configuração do cliente.
Campos não informados são lidos das variáveis de ambiente
`APIBRASIL_BEARER_TOKEN`, `APIBRASIL_DEVICE_TOKEN`, `APIBRASIL_SECRET_KEY`
e `APIBRASIL_BASE_URL`. Credenciais vazias contam como ausentes: informar
`""` é a forma de desligar o que veio do ambiente.
ApiBrasil.new(bearer_token: "jwt", device_token: "device")
# o mesmo, via configuração da aplicação
config :apibrasil,
bearer_token: {:system, "APIBRASIL_BEARER_TOKEN"},
timeout: 60_000
"""
alias ApiBrasil.Core.{Env, Hooks, Retry, Transport, Utils}
@default_base_url "https://gateway.apibrasil.io/api/v2"
@default_timeout 30_000
@type t :: %__MODULE__{
bearer_token: String.t() | nil,
device_token: String.t() | nil,
secret_key: String.t() | nil,
base_url: String.t() | nil,
timeout: non_neg_integer() | nil,
headers: map(),
transport: Transport.t() | nil,
retry: Retry.t() | nil,
hooks: Hooks.t(),
options: keyword()
}
@doc """
Configuração do cliente.
- `:bearer_token` — token JWT obtido no login (`Authorization: Bearer`);
- `:device_token` — token do dispositivo, exigido pelos serviços device-based;
- `:secret_key` — SecretKey da API, usada apenas na criação de devices;
- `:base_url` — base da API. Padrão: `#{@default_base_url}`;
- `:timeout` — tempo limite das requisições em ms. Padrão: `#{@default_timeout}`;
- `:headers` — headers adicionais enviados em todas as requisições;
- `:transport` — camada de transporte HTTP. Padrão: `ApiBrasil.Core.Transport.Httpc`;
- `:retry` — política de retry. Padrão: `%ApiBrasil.Core.Retry{}`;
- `:hooks` — ganchos de observabilidade;
- `:options` — opções aplicadas a todas as chamadas do cliente.
"""
defstruct bearer_token: nil,
device_token: nil,
secret_key: nil,
base_url: nil,
timeout: nil,
headers: %{},
transport: nil,
retry: nil,
hooks: nil,
options: []
@doc "Base da API usada quando nada é informado."
@spec default_base_url() :: String.t()
def default_base_url, do: @default_base_url
@doc "Tempo limite padrão das requisições, em milissegundos."
@spec default_timeout() :: pos_integer()
def default_timeout, do: @default_timeout
@doc "Cria uma configuração a partir de uma keyword list, mapa ou struct."
@spec new(t() | keyword() | map()) :: t()
def new(%__MODULE__{} = config), do: config
def new(fields) when is_list(fields) or is_map(fields) do
fields =
fields
|> Enum.into([])
|> Keyword.new(fn {key, value} -> {normalize_key(key), value} end)
struct!(__MODULE__, fields)
end
@doc """
Lê a configuração das variáveis de ambiente e da configuração da
aplicação (`config :apibrasil, ...`).
"""
@spec from_env() :: t()
def from_env do
application =
:apibrasil
|> Application.get_all_env()
|> Keyword.take([
:bearer_token,
:device_token,
:secret_key,
:base_url,
:timeout,
:headers,
:transport,
:retry,
:hooks,
:options
])
|> Enum.map(fn {key, value} -> {key, resolve_system(value)} end)
# As variáveis de ambiente têm prioridade sobre a config da aplicação.
merge(new(application), %__MODULE__{
bearer_token: Env.get(Env.bearer_token()),
device_token: Env.get(Env.device_token()),
secret_key: Env.get(Env.secret_key()),
base_url: Env.get(Env.base_url())
})
end
# Cada campo segue a mesma regra: o valor de `override` vence quando está
# preenchido. `nil`, `[]` e `%{}` contam como não informados.
@mergeable_fields [
:bearer_token,
:device_token,
:secret_key,
:base_url,
:timeout,
:transport,
:retry,
:hooks,
:headers,
:options
]
@doc """
Devolve esta configuração sobreposta por `override` — os campos
preenchidos em `override` têm prioridade.
"""
@spec merge(t(), t()) :: t()
def merge(%__MODULE__{} = base, %__MODULE__{} = override) do
Enum.reduce(@mergeable_fields, base, fn field, merged ->
case Map.fetch!(override, field) do
value when value in [nil, [], %{}] -> merged
value -> Map.put(merged, field, value)
end
end)
end
@doc """
Resolve a configuração com o ambiente e devolve o cliente pronto para uso.
"""
@spec resolve(t() | keyword() | map()) :: ApiBrasil.Client.t()
def resolve(config) do
resolved = merge(from_env(), new(config))
%ApiBrasil.Client{
bearer_token: Utils.presence(resolved.bearer_token),
device_token: Utils.presence(resolved.device_token),
secret_key: Utils.presence(resolved.secret_key),
base_url: Utils.presence(resolved.base_url) || @default_base_url,
timeout: resolved.timeout || @default_timeout,
headers: Utils.normalize_headers(resolved.headers),
transport: resolved.transport || ApiBrasil.Core.Transport.Httpc,
retry: Retry.new(resolved.retry || %Retry{}),
hooks: resolved.hooks,
options: resolved.options || []
}
end
@doc "Devolve a configuração equivalente a um cliente já montado."
@spec from_client(ApiBrasil.Client.t()) :: t()
def from_client(%ApiBrasil.Client{} = client) do
%__MODULE__{
bearer_token: client.bearer_token,
device_token: client.device_token,
secret_key: client.secret_key,
base_url: client.base_url,
timeout: client.timeout,
headers: client.headers,
transport: client.transport,
retry: client.retry,
hooks: client.hooks,
options: client.options
}
end
defp resolve_system({:system, name}), do: Env.get(name)
defp resolve_system({:system, name, default}), do: Env.get(name) || default
defp resolve_system(value), do: value
defp normalize_key(key) when is_atom(key), do: key
defp normalize_key(key) when is_binary(key), do: String.to_existing_atom(key)
end