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 mix tasks apibrasil.codegen.ex
Raw

lib/mix/tasks/apibrasil.codegen.ex

defmodule Mix.Tasks.Apibrasil.Codegen do
use Mix.Task
@shortdoc "Regenera o catálogo de endpoints da APIBrasil"
@moduledoc """
Gera `lib/api_brasil/generated/catalog.ex` a partir do catálogo público do
gateway APIBrasil (`GET /api/v2/documentations`).
mix apibrasil.codegen
mix apibrasil.codegen caminho/catalog.ex
A base pode ser trocada pela variável de ambiente `APIBRASIL_BASE_URL`.
A task percorre `endpoints[].url` de cada documentação, agrupa as actions
por serviço, separa os serviços de consulta por crédito
(`consulta/{servico}/credits`) e lê os tipos de consulta do body de exemplo
(campo `tipo`, sem as chaves `tipo` e `homolog` nos campos). Tudo sai
ordenado alfabeticamente: regenerar sem mudanças no gateway produz
exatamente o mesmo arquivo.
"""
alias ApiBrasil.Platform.Catalog
@requirements ["app.start"]
@default_output Path.join(["lib", "api_brasil", "generated", "catalog.ex"])
@timeout 120_000
@line_length 98
@functions """
@doc "Fonte do catálogo."
@spec source() :: String.t()
def source, do: @source
@doc "Actions conhecidas da API de WhatsApp (`POST /whatsapp/{action}`)."
@spec whatsapp_actions() :: [String.t()]
def whatsapp_actions, do: @whatsapp_actions
@doc \"\"\"
Caminhos conhecidos da Evolution API
(`POST /evolution/{controller}/{action}`).
\"\"\"
@spec evolution_paths() :: [String.t()]
def evolution_paths, do: @evolution_paths
@doc "Actions conhecidas do WhatsMeow (`POST /whatsmeow/{action}`)."
@spec whatsmeow_actions() :: [String.t()]
def whatsmeow_actions, do: @whatsmeow_actions
@doc "Serviços de consulta por crédito (`POST /consulta/{servico}/credits`)."
@spec consulta_servicos() :: [String.t()]
def consulta_servicos, do: @consulta_servicos
@doc "Metadados por tipo de consulta (campo `tipo` do body), ordenados por tipo."
@spec consulta_tipos() :: %{String.t() => %{service: String.t(), fields: [String.t()]}}
def consulta_tipos, do: @consulta_tipos
@doc \"\"\"
Metadados de um tipo de consulta — o serviço da rota
(`POST /consulta/{servico}/credits`) e os campos do body de exemplo, fora
`tipo` e `homolog`. Devolve `nil` quando o tipo não está no catálogo.
ApiBrasil.Generated.Catalog.consulta_tipo("acerta-essencial")
#=> %{service: "cpf", fields: ["cpf"]}
\"\"\"
@spec consulta_tipo(String.t()) :: %{service: String.t(), fields: [String.t()]} | nil
def consulta_tipo(tipo), do: Map.get(@consulta_tipos, to_string(tipo))
@doc "Actions documentadas por serviço do gateway, ordenadas por serviço."
@spec service_actions() :: %{String.t() => [String.t()]}
def service_actions, do: @service_actions
@doc \"\"\"
Actions documentadas de um serviço do gateway — lista vazia quando o
serviço não está no catálogo.
\"\"\"
@spec service_actions(String.t()) :: [String.t()]
def service_actions(service), do: Map.get(@service_actions, to_string(service), [])
@doc "Informa se uma action é documentada para o serviço."
@spec has_action?(String.t(), String.t()) :: boolean()
def has_action?(service, action), do: to_string(action) in service_actions(service)
@doc "Serviços do gateway presentes no catálogo, em ordem alfabética."
@spec services() :: [String.t()]
def services, do: @services
end
"""
@impl Mix.Task
@spec run([String.t()]) :: :ok
def run(args) do
output = output_path(args)
client = ApiBrasil.new(timeout: @timeout)
source = ApiBrasil.url(client, "documentations")
Mix.shell().info("Baixando catálogo de #{source} ...")
documentations = documentations!(client)
catalog = collect(documentations)
write!(output, render(source, length(documentations), catalog))
Mix.shell().info(
"OK: #{output} (#{length(documentations)} docs, #{catalog.endpoints} endpoints, " <>
"#{map_size(catalog.consulta_tipos)} tipos)"
)
end
defp output_path([output | _rest]) when is_binary(output) and output != "", do: output
defp output_path(_args), do: @default_output
defp documentations!(client) do
case Catalog.documentations(client) do
{:ok, response} ->
case extract_documentations(response) do
[] -> Mix.raise("O catálogo respondeu sem documentações.")
documentations -> documentations
end
{:error, error} ->
Mix.raise("Falha ao baixar o catálogo: #{Exception.message(error)}")
end
end
# O gateway responde `{"documentations": [...]}` ou uma lista direta — que a
# SDK normaliza em `{"data": [...]}`.
defp extract_documentations(response) do
Enum.find_value(["documentations", "data"], [], fn key ->
case Map.get(response, key) do
items when is_list(items) -> items
_other -> nil
end
end)
end
defp collect(documentations) do
initial = %{
endpoints: 0,
service_actions: %{},
consulta_servicos: MapSet.new(),
consulta_tipos: %{}
}
documentations
|> Enum.flat_map(&endpoints/1)
|> Enum.reduce(initial, &collect_endpoint/2)
end
defp endpoints(%{"endpoints" => endpoints}) when is_list(endpoints), do: endpoints
defp endpoints(_documentation), do: []
defp collect_endpoint(endpoint, acc) when is_map(endpoint) do
case api_path(Map.get(endpoint, "url")) do
nil -> acc
path -> collect_path(endpoint, path, %{acc | endpoints: acc.endpoints + 1})
end
end
defp collect_endpoint(_endpoint, acc), do: acc
defp collect_path(endpoint, path, acc) do
case split_path(path) do
{"", _action} ->
acc
{service, action} ->
acc = put_action(acc, service, action)
case consulta_service(path) do
nil -> acc
consulta -> put_consulta(acc, endpoint, consulta)
end
end
end
defp put_action(acc, service, action) do
actions = Map.get(acc.service_actions, service, MapSet.new())
actions = if action == "", do: actions, else: MapSet.put(actions, action)
%{acc | service_actions: Map.put(acc.service_actions, service, actions)}
end
defp put_consulta(acc, endpoint, consulta) do
acc = %{acc | consulta_servicos: MapSet.put(acc.consulta_servicos, consulta)}
with %{} = body <- Map.get(endpoint, "body"),
tipo when is_binary(tipo) and tipo != "" <- Map.get(body, "tipo") do
fields =
body
|> Map.keys()
|> Enum.reject(&(&1 in ["tipo", "homolog"]))
|> Enum.sort()
tipos = Map.put(acc.consulta_tipos, tipo, %{service: consulta, fields: fields})
%{acc | consulta_tipos: tipos}
else
_other -> acc
end
end
# Extrai o caminho depois de `/api/v2/`.
defp api_path(url) when is_binary(url) do
case String.split(url, "/api/v2/", parts: 2) do
[_base, path] ->
case String.trim(path, "/") do
"" -> nil
path -> path
end
_other ->
nil
end
end
defp api_path(_url), do: nil
defp split_path(path) do
case String.split(path, "/", parts: 2) do
[service, action] -> {service, action}
[service] -> {service, ""}
end
end
# Devolve o serviço de uma rota `consulta/{servico}/credits`.
defp consulta_service("consulta/" <> rest) do
case String.split(rest, "/") do
[service, "credits"] when service != "" -> service
_other -> nil
end
end
defp consulta_service(_path), do: nil
defp write!(output, content) do
with :ok <- output |> Path.dirname() |> File.mkdir_p(),
:ok <- File.write(output, content) do
:ok
else
{:error, reason} ->
Mix.raise("Falha ao escrever o catálogo em #{output}: #{:file.format_error(reason)}")
end
end
defp render(source, doc_count, catalog) do
service_actions =
Map.new(catalog.service_actions, fn {service, actions} ->
{service, Enum.sort(MapSet.to_list(actions))}
end)
IO.iodata_to_binary([
header(source, doc_count, catalog.endpoints, map_size(catalog.consulta_tipos)),
list_attribute("whatsapp_actions", Map.get(service_actions, "whatsapp", [])),
list_attribute("evolution_paths", Map.get(service_actions, "evolution", [])),
list_attribute("whatsmeow_actions", Map.get(service_actions, "whatsmeow", [])),
list_attribute("consulta_servicos", Enum.sort(catalog.consulta_servicos)),
map_attribute("consulta_tipos", Enum.sort(catalog.consulta_tipos), &tipo_entry/1),
map_attribute("service_actions", Enum.sort(service_actions), &actions_entry/1),
" @services Enum.sort(Map.keys(@service_actions))\n\n",
@functions
])
end
defp header(source, doc_count, endpoint_count, tipo_count) do
resumo =
"#{doc_count} documentações, #{endpoint_count} endpoints, " <>
"#{tipo_count} tipos de consulta conhecidos."
"""
defmodule ApiBrasil.Generated.Catalog do
@moduledoc \"\"\"
Catálogo de endpoints conhecidos da plataforma APIBrasil —
ARQUIVO GERADO AUTOMATICAMENTE, não edite.
Fonte: <#{source}>
Regenerar: `mix apibrasil.codegen`
#{resumo}
ApiBrasil.Generated.Catalog.has_action?("whatsapp", "sendText")
#=> true
ApiBrasil.Generated.Catalog.service_actions("correios")
#=> ["rastreio"]
ApiBrasil.Generated.Catalog.consulta_tipo("acerta-essencial")
#=> %{service: "cpf", fields: ["cpf"]}
\"\"\"
@source #{quote_string(source)}
"""
end
defp list_attribute(name, []), do: " @#{name} []\n\n"
defp list_attribute(name, values) do
entries = Enum.map_join(values, ",\n", &(" " <> quote_string(&1)))
" @#{name} [\n" <> entries <> "\n ]\n\n"
end
defp map_attribute(name, [], _entry), do: " @#{name} %{}\n\n"
defp map_attribute(name, entries, entry) do
" @#{name} %{\n" <> Enum.map_join(entries, ",\n", entry) <> "\n }\n\n"
end
defp tipo_entry({tipo, meta}) do
inline =
" " <>
quote_string(tipo) <>
" => %{service: " <>
quote_string(meta.service) <> ", fields: " <> inline_list(meta.fields) <> "}"
if fits_entry?(inline) do
inline
else
" " <>
quote_string(tipo) <>
" => %{\n service: " <>
quote_string(meta.service) <> ",\n" <> fields_block(meta.fields) <> "\n }"
end
end
defp fields_block(fields) do
inline = " fields: " <> inline_list(fields)
if fits?(inline) do
inline
else
" fields: [\n" <>
Enum.map_join(fields, ",\n", &(" " <> quote_string(&1))) <> "\n ]"
end
end
defp actions_entry({service, actions}) do
inline = " " <> quote_string(service) <> " => " <> inline_list(actions)
if fits_entry?(inline) do
inline
else
" " <>
quote_string(service) <>
" => [\n" <> Enum.map_join(actions, ",\n", &(" " <> quote_string(&1))) <> "\n ]"
end
end
defp inline_list(values), do: "[" <> Enum.map_join(values, ", ", &quote_string/1) <> "]"
# A vírgula que separa os itens entra na conta da largura da linha.
defp fits_entry?(line), do: String.length(line) + 1 <= @line_length
defp fits?(line), do: String.length(line) <= @line_length
defp quote_string(value) do
escaped =
value
|> String.replace("\\", "\\\\")
|> String.replace("\"", "\\\"")
|> String.replace("\n", "\\n")
|> String.replace("\r", "\\r")
|> String.replace("\t", "\\t")
|> String.replace("\#{", "\\\#{")
"\"" <> escaped <> "\""
end
end