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