Packages

SDK oficial Elixir da plataforma APIBrasil: WhatsApp, SMS, consultas de CPF/CNPJ, veiculos, CEP, correios, pagamentos PIX/boleto e mais.

Current section

Files

Jump to
apibrasil lib api_brasil core service.ex
Raw

lib/api_brasil/core/service.ex

defmodule ApiBrasil.Core.Service do
@moduledoc """
Base compartilhada por todos os serviços da SDK.
Reúne duas coisas:
- **helpers de runtime** (`get/3`, `post/4`, `device/5`, `credit/4`...),
usados tanto pelo código gerado quanto pelos serviços escritos à mão;
- **uma DSL** (`use ApiBrasil.Core.Service`) que declara as rotas a partir
de uma tabela — as rotas device-based e as consultas por crédito são
perfeitamente uniformes, então descrevê-las é mais claro (e menos
sujeito a divergência) do que repetir o mesmo corpo em cada função.
## Declarando um serviço
defmodule ApiBrasil.Messaging.WhatsApp do
use ApiBrasil.Core.Service, device: "whatsapp"
action(:send_text, "sendText", "Envia uma mensagem de texto.")
end
Cada rota declarada gera **duas** funções: a que devolve
`{:ok, resultado} | {:error, %ApiBrasil.Core.Error{}}` e a variante `!`,
que devolve o resultado direto e levanta em caso de falha.
## Opções do `use`
- `device: "whatsapp"` — serviço device-based: gera `service/0`,
`request/4` (qualquer action do catálogo) e `queue/4` (a mesma action
por fila). As rotas são declaradas com `action/3`.
- `credit: true` — consultas por crédito: gera `generic/4` e `credits/3`.
As rotas são declaradas com `credit/3`.
- sem opções — serviço da plataforma: apenas os helpers e as macros
`route_get/3`, `route_post/3`, `route_put/3`, `route_delete/3` e
`route_empty/4`.
"""
alias ApiBrasil.Client
alias ApiBrasil.Core.{CreditResponse, DeviceResponse, Error, HTTP, Transport, Utils}
@type result :: {:ok, map()} | {:error, Error.t()}
# ----------------------------------------------------------------------
# Helpers de runtime
# ----------------------------------------------------------------------
@doc "Executa uma requisição arbitrária no gateway."
@spec request(Client.t(), Transport.method(), String.t(), term(), keyword()) :: result()
def request(client, method, path, body \\ nil, opts \\ []),
do: HTTP.request_json(client, method, path, body, opts)
@doc "`GET path`."
@spec get(Client.t(), String.t(), keyword()) :: result()
def get(client, path, opts \\ []), do: HTTP.get(client, path, opts)
@doc "`GET path` com a query mesclada às opções da chamada."
@spec get_query(Client.t(), String.t(), map() | keyword() | nil, keyword()) :: result()
def get_query(client, path, query, opts \\ []) do
HTTP.get(client, path, HTTP.merge_options([query: query], opts))
end
@doc "`POST path`."
@spec post(Client.t(), String.t(), term(), keyword()) :: result()
def post(client, path, body \\ nil, opts \\ []), do: HTTP.post(client, path, body, opts)
@doc "`PUT path`."
@spec put(Client.t(), String.t(), term(), keyword()) :: result()
def put(client, path, body \\ nil, opts \\ []), do: HTTP.put(client, path, body, opts)
@doc "`PATCH path`."
@spec patch(Client.t(), String.t(), term(), keyword()) :: result()
def patch(client, path, body \\ nil, opts \\ []), do: HTTP.patch(client, path, body, opts)
@doc "`DELETE path`."
@spec delete(Client.t(), String.t(), term(), keyword()) :: result()
def delete(client, path, body \\ nil, opts \\ []), do: HTTP.delete(client, path, body, opts)
@doc "Baixa os bytes crus de uma rota (PDF de boleto, imagens...)."
@spec download(Client.t(), String.t(), keyword()) :: {:ok, binary()} | {:error, Error.t()}
def download(client, path, opts \\ []), do: HTTP.bytes(client, :get, path, nil, opts)
@doc """
Executa uma action device-based: `POST /{servico}/{action}`.
Devolve o envelope `ApiBrasil.Core.DeviceResponse`.
"""
@spec device(Client.t(), String.t(), String.t(), term(), keyword()) ::
{:ok, DeviceResponse.t()} | {:error, Error.t()}
def device(client, service, action, body \\ nil, opts \\ []) do
with {:ok, json} <- HTTP.post(client, device_path(service, action), body, opts) do
{:ok, DeviceResponse.new(json)}
end
end
@doc "Executa a action device-based por fila: `POST /{servico}/{action}/queue`."
@spec device_queue(Client.t(), String.t(), String.t(), term(), keyword()) ::
{:ok, DeviceResponse.t()} | {:error, Error.t()}
def device_queue(client, service, action, body \\ nil, opts \\ []) do
device(client, service, String.trim_trailing(to_string(action), "/") <> "/queue", body, opts)
end
@doc """
Executa uma consulta por crédito: `POST /consulta/{servico}/credits`.
Devolve o envelope `ApiBrasil.Core.CreditResponse`.
"""
@spec credit_request(Client.t(), String.t(), term(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def credit_request(client, service, body \\ nil, opts \\ []) do
credit_post(client, "consulta/#{service}/credits", body, opts)
end
@doc "Consulta os créditos disponíveis de um serviço: `GET /consulta/{servico}/credits`."
@spec credit_balance(Client.t(), String.t(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def credit_balance(client, service, opts \\ []) do
with {:ok, json} <- HTTP.get(client, "consulta/#{service}/credits", opts) do
{:ok, CreditResponse.new(json)}
end
end
@doc """
Faz um `POST` e embrulha a resposta no envelope das consultas por crédito —
atalho para rotas fora do padrão `/consulta/{servico}/credits`.
"""
@spec credit_post(Client.t(), String.t(), term(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def credit_post(client, path, body \\ nil, opts \\ []) do
with {:ok, json} <- HTTP.post(client, path, body, opts) do
{:ok, CreditResponse.new(json)}
end
end
@doc """
Desembrulha um resultado: devolve o valor de `{:ok, valor}` e levanta o
`ApiBrasil.Core.Error` de `{:error, erro}`.
É o que as variantes `!` usam.
"""
@spec unwrap!({:ok, value} | {:error, Error.t()}) :: value when value: term()
def unwrap!({:ok, value}), do: value
def unwrap!({:error, %Error{} = error}), do: raise(error)
@doc "Monta o caminho de uma action device-based."
@spec device_path(String.t(), String.t() | nil) :: String.t()
def device_path(service, action) do
case action |> to_string() |> String.trim("/") do
"" -> service
action -> "#{service}/#{action}"
end
end
@doc "Injeta a SecretKey do cliente nas opções quando ela não foi informada."
@spec with_secret_key(Client.t(), keyword()) :: keyword()
def with_secret_key(%Client{secret_key: secret_key}, opts) do
case {Keyword.get(opts, :secret_key), secret_key} do
{nil, nil} -> Keyword.delete(opts, :secret_key)
{nil, secret_key} -> Keyword.put(opts, :secret_key, secret_key)
{_informada, _cliente} -> opts
end
end
@doc "Codifica um segmento de caminho de URL."
@spec encode_path(term()) :: String.t()
def encode_path(value), do: Utils.encode_path(to_string(value))
# ----------------------------------------------------------------------
# DSL
# ----------------------------------------------------------------------
@doc false
defmacro __using__(opts) do
device = Keyword.get(opts, :device)
credit = Keyword.get(opts, :credit, false)
base =
quote do
import ApiBrasil.Core.Service,
only: [
action: 3,
credit: 3,
route_get: 3,
route_post: 3,
route_put: 3,
route_delete: 3,
route_empty: 4
]
alias ApiBrasil.Client
alias ApiBrasil.Core.{CreditResponse, DeviceResponse, Error, Service}
@type result :: {:ok, map()} | {:error, Error.t()}
end
parts =
[base] ++
if(device, do: [device_base(device)], else: []) ++
if(credit, do: [credit_base()], else: [])
quote do
(unquote_splicing(parts))
end
end
defp device_base(service) do
quote do
@doc "Serviço do gateway coberto por este módulo: `#{unquote(service)}`."
@spec service() :: String.t()
def service, do: unquote(service)
@doc """
Executa qualquer action do serviço: `POST /#{unquote(service)}/{action}`.
As actions conhecidas estão em
`ApiBrasil.Generated.Catalog.service_actions("#{unquote(service)}")`; a
documentação completa fica em <https://doc.apibrasil.io>.
"""
@spec request(Client.t(), String.t(), term(), keyword()) ::
{:ok, DeviceResponse.t()} | {:error, Error.t()}
def request(client, action_name, body \\ nil, opts \\ []) do
unquote(__MODULE__).device(client, unquote(service), action_name, body, opts)
end
@doc "Como `request/4`, mas levanta `ApiBrasil.Core.Error` em caso de falha."
@spec request!(Client.t(), String.t(), term(), keyword()) :: DeviceResponse.t()
def request!(client, action_name, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(request(client, action_name, body, opts))
end
@doc "Executa a action de forma assíncrona, por fila: `POST /#{unquote(service)}/{action}/queue`."
@spec queue(Client.t(), String.t(), term(), keyword()) ::
{:ok, DeviceResponse.t()} | {:error, Error.t()}
def queue(client, action_name, body \\ nil, opts \\ []) do
unquote(__MODULE__).device_queue(client, unquote(service), action_name, body, opts)
end
@doc "Como `queue/4`, mas levanta `ApiBrasil.Core.Error` em caso de falha."
@spec queue!(Client.t(), String.t(), term(), keyword()) :: DeviceResponse.t()
def queue!(client, action_name, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(queue(client, action_name, body, opts))
end
end
end
defp credit_base do
quote do
@doc """
Executa uma consulta genérica: `POST /consulta/{servico}/credits`.
Os serviços conhecidos estão em
`ApiBrasil.Generated.Catalog.consulta_servicos/0`.
"""
@spec generic(Client.t(), String.t(), term(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def generic(client, service, body \\ nil, opts \\ []) do
unquote(__MODULE__).credit_request(client, service, body, opts)
end
@doc "Como `generic/4`, mas levanta `ApiBrasil.Core.Error` em caso de falha."
@spec generic!(Client.t(), String.t(), term(), keyword()) :: CreditResponse.t()
def generic!(client, service, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(generic(client, service, body, opts))
end
@doc "Consulta os créditos disponíveis de um serviço: `GET /consulta/{servico}/credits`."
@spec credits(Client.t(), String.t(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def credits(client, service, opts \\ []) do
unquote(__MODULE__).credit_balance(client, service, opts)
end
@doc "Como `credits/3`, mas levanta `ApiBrasil.Core.Error` em caso de falha."
@spec credits!(Client.t(), String.t(), keyword()) :: CreditResponse.t()
def credits!(client, service, opts \\ []) do
unquote(__MODULE__).unwrap!(credits(client, service, opts))
end
end
end
@doc """
Declara uma action device-based: `POST /{servico}/{action}`.
action(:send_text, "sendText", "Envia uma mensagem de texto.")
Gera `send_text/3` (`{:ok, envelope} | {:error, erro}`) e `send_text!/3`.
"""
defmacro action(name, path, doc) do
bang = bang_name(name)
bang_doc = bang_doc(name, 3)
quote do
@doc unquote(doc)
@spec unquote(name)(Client.t(), term(), keyword()) ::
{:ok, DeviceResponse.t()} | {:error, Error.t()}
def unquote(name)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).device(client, service(), unquote(path), body, opts)
end
@doc unquote(bang_doc)
@spec unquote(bang)(Client.t(), term(), keyword()) :: DeviceResponse.t()
def unquote(bang)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(unquote(name)(client, body, opts))
end
end
end
@doc """
Declara uma consulta por crédito: `POST /consulta/{servico}/credits`.
credit(:cpf, "cpf", "Consulta um CPF.")
Gera `cpf/3` (`{:ok, envelope} | {:error, erro}`) e `cpf!/3`.
"""
defmacro credit(name, service, doc) do
bang = bang_name(name)
bang_doc = bang_doc(name, 3)
quote do
@doc unquote(doc)
@spec unquote(name)(Client.t(), term(), keyword()) ::
{:ok, CreditResponse.t()} | {:error, Error.t()}
def unquote(name)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).credit_request(client, unquote(service), body, opts)
end
@doc unquote(bang_doc)
@spec unquote(bang)(Client.t(), term(), keyword()) :: CreditResponse.t()
def unquote(bang)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(unquote(name)(client, body, opts))
end
end
end
@doc """
Declara uma rota `GET` sem body.
route_get(:balance, "balance", "Saldo/créditos da conta.")
Gera `balance/2` e `balance!/2`.
"""
defmacro route_get(name, path, doc) do
bang = bang_name(name)
bang_doc = bang_doc(name, 2)
quote do
@doc unquote(doc)
@spec unquote(name)(Client.t(), keyword()) :: result()
def unquote(name)(client, opts \\ []) do
unquote(__MODULE__).get(client, unquote(path), opts)
end
@doc unquote(bang_doc)
@spec unquote(bang)(Client.t(), keyword()) :: map()
def unquote(bang)(client, opts \\ []) do
unquote(__MODULE__).unwrap!(unquote(name)(client, opts))
end
end
end
@doc """
Declara uma rota `POST` com body.
route_post(:recharge, "recharge", "Cria uma recarga.")
Gera `recharge/3` e `recharge!/3`.
"""
defmacro route_post(name, path, doc), do: body_route(name, :post, path, doc)
@doc "Declara uma rota `PUT` com body. Gera `nome/3` e `nome!/3`."
defmacro route_put(name, path, doc), do: body_route(name, :put, path, doc)
@doc "Declara uma rota `DELETE` com body. Gera `nome/3` e `nome!/3`."
defmacro route_delete(name, path, doc), do: body_route(name, :delete, path, doc)
@doc """
Declara uma rota sem body e sem parâmetros, no verbo informado.
route_empty(:token_rotate, :post, "auth/token/rotate", "Rotaciona o token.")
Gera `token_rotate/2` e `token_rotate!/2`.
"""
defmacro route_empty(name, verb, path, doc) do
bang = bang_name(name)
bang_doc = bang_doc(name, 2)
quote do
@doc unquote(doc)
@spec unquote(name)(Client.t(), keyword()) :: result()
def unquote(name)(client, opts \\ []) do
unquote(__MODULE__).request(client, unquote(verb), unquote(path), nil, opts)
end
@doc unquote(bang_doc)
@spec unquote(bang)(Client.t(), keyword()) :: map()
def unquote(bang)(client, opts \\ []) do
unquote(__MODULE__).unwrap!(unquote(name)(client, opts))
end
end
end
defp body_route(name, verb, path, doc) do
bang = bang_name(name)
bang_doc = bang_doc(name, 3)
quote do
@doc unquote(doc)
@spec unquote(name)(Client.t(), term(), keyword()) :: result()
def unquote(name)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).request(client, unquote(verb), unquote(path), body, opts)
end
@doc unquote(bang_doc)
@spec unquote(bang)(Client.t(), term(), keyword()) :: map()
def unquote(bang)(client, body \\ nil, opts \\ []) do
unquote(__MODULE__).unwrap!(unquote(name)(client, body, opts))
end
end
end
defp bang_name(name) when is_atom(name), do: :"#{name}!"
defp bang_doc(name, arity) do
"Como `#{name}/#{arity}`, mas devolve o resultado direto e levanta " <>
"`ApiBrasil.Core.Error` em caso de falha."
end
end