Current section
Files
Jump to
Current section
Files
lib/api_brasil/core/transport.ex
defmodule ApiBrasil.Core.Transport do
@moduledoc """
Camada de transporte HTTP plugável.
A implementação padrão é `ApiBrasil.Core.Transport.Httpc` (`:httpc`, do
Erlang/OTP — sem dependências). Injete a sua para usar proxies
corporativos, instrumentação, Finch/Req/Tesla ou mocks de teste.
Um transporte pode ser:
- um **módulo** que implementa este behaviour;
- uma tupla **`{módulo, opções}`**, com as opções repassadas em cada chamada;
- uma **função** de aridade 1, que recebe a `t:request/0` e devolve
`{:ok, resposta}` ou `{:error, erro}` — o atalho para testes.
## Contrato
Devolve `{:ok, %Response{}}` para **qualquer** status HTTP; devolve
`{:error, %ApiBrasil.Core.Error{}}` (`:network` ou `:timeout`) apenas
quando não houve resposta.
defmodule MeuTransporte do
@behaviour ApiBrasil.Core.Transport
alias ApiBrasil.Core.Transport.Response
@impl true
def request(%ApiBrasil.Core.Transport.Request{} = request, _opts) do
{:ok, Response.json(200, %{"ok" => true, "url" => request.url})}
end
end
ApiBrasil.new(transport: MeuTransporte)
"""
alias ApiBrasil.Core.{Error, JSON}
defmodule Request do
@moduledoc "Requisição entregue à camada de transporte."
alias ApiBrasil.Core.JSON
@type t :: %__MODULE__{
method: ApiBrasil.Core.Transport.method(),
url: String.t(),
headers: %{String.t() => String.t()},
body: binary() | nil,
timeout: non_neg_integer() | nil,
response_type: :json | :binary
}
defstruct method: :get,
url: "",
headers: %{},
body: nil,
timeout: nil,
response_type: :json
@doc "Devolve o body decodificado como JSON, quando houver."
@spec json_body(t()) :: term()
def json_body(%__MODULE__{body: nil}), do: nil
def json_body(%__MODULE__{body: body}) do
case JSON.decode(body) do
{:ok, value} -> value
{:error, _reason} -> nil
end
end
end
defmodule Response do
@moduledoc "Resposta devolvida pela camada de transporte."
alias ApiBrasil.Core.JSON
@type t :: %__MODULE__{
status: pos_integer(),
headers: %{String.t() => String.t()},
data: term(),
body: binary()
}
defstruct status: 200, headers: %{}, data: nil, body: ""
@doc "Monta uma resposta com status e corpo JSON — atalho para testes."
@spec json(pos_integer(), term(), map()) :: t()
def json(status, data, headers \\ %{}) do
%__MODULE__{
status: status,
headers: headers,
data: data,
body: JSON.encode!(data)
}
end
@doc "Adiciona um header à resposta."
@spec put_header(t(), String.t(), String.t()) :: t()
def put_header(%__MODULE__{} = response, name, value) do
%{response | headers: Map.put(response.headers, String.downcase(name), value)}
end
end
@typedoc "Verbo HTTP usado pelo gateway."
@type method :: :get | :post | :put | :patch | :delete
@typedoc "Transporte aceito pela configuração."
@type t ::
module()
| {module(), keyword()}
| (Request.t() -> {:ok, Response.t()} | {:error, Error.t()})
@doc "Executa a requisição HTTP."
@callback request(Request.t(), keyword()) :: {:ok, Response.t()} | {:error, Error.t()}
@doc "Verbos HTTP suportados."
@spec methods() :: [method()]
def methods, do: [:get, :post, :put, :patch, :delete]
@doc "Devolve o verbo em maiúsculas — `GET`, `POST`, `PUT`, `PATCH` ou `DELETE`."
@spec method_to_string(method()) :: String.t()
def method_to_string(method), do: method |> Atom.to_string() |> String.upcase()
@doc """
Despacha a requisição para o transporte configurado — módulo,
`{módulo, opções}` ou função de aridade 1.
"""
@spec call(t(), Request.t()) :: {:ok, Response.t()} | {:error, Error.t()}
def call(transport, %Request{} = request) when is_function(transport, 1) do
transport.(request)
end
def call({module, opts}, %Request{} = request) when is_atom(module) and is_list(opts) do
module.request(request, opts)
end
def call(module, %Request{} = request) when is_atom(module) do
module.request(request, [])
end
@doc """
Decodifica o corpo cru conforme o tipo de resposta: JSON vira mapa/lista,
corpos não-JSON viram texto e `:binary` devolve os próprios bytes.
"""
@spec decode_body(binary(), :json | :binary) :: term()
def decode_body(raw, :binary), do: raw
def decode_body("", :json), do: nil
def decode_body(raw, :json) do
case JSON.decode(raw) do
{:ok, value} -> value
{:error, _reason} -> raw
end
end
end