defmodule ApiBrasil.Core.Utils do @moduledoc """ Utilidades compartilhadas: montagem de URL, query string, normalização de headers e conversão de bodies. """ alias ApiBrasil.Core.JSON @doc """ Junta a base da API com o caminho, sem barras duplicadas. iex> ApiBrasil.Core.Utils.join_url("https://gateway.apibrasil.io/api/v2/", "/whatsapp/sendText") "https://gateway.apibrasil.io/api/v2/whatsapp/sendText" """ @spec join_url(String.t(), String.t()) :: String.t() def join_url(base_url, path) do base = String.trim_trailing(base_url, "/") suffix = String.trim_leading(path || "", "/") if suffix == "", do: base, else: base <> "/" <> suffix end @doc """ Monta a query string a partir de um mapa/keyword, ignorando valores nulos. As chaves saem ordenadas, o que torna a URL determinística. Devolve `""` quando não há nada a enviar. iex> ApiBrasil.Core.Utils.build_query_string(%{"search" => "cep", "page" => 2}) "?page=2&search=cep" """ @spec build_query_string(map() | keyword() | nil) :: String.t() def build_query_string(nil), do: "" def build_query_string([]), do: "" def build_query_string(query) when is_map(query) and map_size(query) == 0, do: "" def build_query_string(query) when is_map(query) or is_list(query) do pairs = query |> Enum.map(fn {key, value} -> {to_string(key), value} end) |> Enum.reject(fn {_key, value} -> is_nil(value) end) |> Enum.sort_by(fn {key, _value} -> key end) |> Enum.map(fn {key, value} -> URI.encode_www_form(key) <> "=" <> URI.encode_www_form(query_value(value)) end) if pairs == [], do: "", else: "?" <> Enum.join(pairs, "&") end @doc "Codifica um segmento de caminho de URL (mantém `/`)." @spec encode_path(String.t()) :: String.t() def encode_path(value) do URI.encode(to_string(value), &keep_in_path?/1) end defp keep_in_path?(char) do char in ?A..?Z or char in ?a..?z or char in ?0..?9 or char in ~c"-_.~/" end @doc """ Normaliza o corpo de uma requisição. Aceita mapas, keyword lists, structs `ApiBrasil.Consulta`, `nil` e `[]` — devolve um mapa ou `nil` quando não há corpo. """ @spec to_body(term()) :: map() | list() | nil def to_body(nil), do: nil def to_body([]), do: nil def to_body(%ApiBrasil.Consulta{} = consulta), do: ApiBrasil.Consulta.to_map(consulta) def to_body(%_struct{} = struct), do: Map.from_struct(struct) |> stringify_keys() def to_body(body) when is_map(body) and map_size(body) == 0, do: nil def to_body(body) when is_map(body), do: stringify_keys(body) def to_body(body) when is_list(body), do: list_to_body(body) def to_body(body), do: body @doc """ Converte as chaves de um mapa (um nível) em strings — aceita átomos e strings misturados. """ @spec stringify_keys(map()) :: map() def stringify_keys(map) when is_map(map) do Map.new(map, fn {key, value} -> {to_string(key), value} end) end @doc """ Normaliza headers para um mapa `%{"nome" => "valor"}`, preservando o nome original mas removendo duplicatas por comparação sem diferenciar caixa. """ @spec normalize_headers(map() | keyword() | nil) :: map() def normalize_headers(nil), do: %{} def normalize_headers(headers) when is_map(headers) or is_list(headers) do Map.new(headers, fn {key, value} -> {to_string(key), to_string(value)} end) end @doc """ Mescla dois conjuntos de headers sem diferenciar maiúsculas de minúsculas — os de `override` vencem, inclusive quando só a caixa difere. """ @spec merge_headers(map(), map()) :: map() def merge_headers(base, override) do downcased = Map.new(override, fn {key, value} -> {String.downcase(key), value} end) base |> Enum.reject(fn {key, _value} -> Map.has_key?(downcased, String.downcase(key)) end) |> Map.new() |> Map.merge(override) end @doc """ Normaliza a resposta em um mapa JSON. Respostas vazias viram `%{}`; respostas que não são objetos (listas, texto) são embrulhadas em `%{"data" => ...}`. """ @spec to_json_object(term()) :: map() def to_json_object(nil), do: %{} def to_json_object(value) when is_map(value), do: value def to_json_object(value), do: %{"data" => value} @doc "Devolve `nil` para strings vazias — credenciais vazias contam como ausentes." @spec presence(term()) :: term() def presence(nil), do: nil def presence(""), do: nil def presence(value), do: value defp list_to_body(body) do if Keyword.keyword?(body) do body |> Map.new() |> stringify_keys() else body end end defp query_value(value) when is_binary(value), do: value defp query_value(value) when is_atom(value), do: to_string(value) defp query_value(value) when is_integer(value) or is_float(value), do: to_string(value) defp query_value(value), do: JSON.encode!(value) end