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 . """ @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