summaryrefslogtreecommitdiffstats
path: root/lib/cepis.ex
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:21:38 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:21:38 -0300
commitf9f1d248f3a3a2b781507195e00527db1e39f8d3 (patch)
treef99b24f5923e436339b8e8ddb8af6ac854488439 /lib/cepis.ex
parent075683ff44a254e99adbad8a0372ef80ff06151a (diff)
downloadelixir-071f0ea39f80d7dc8a81331e56ec1d9bb936893e.tar.gz
Carry the municipality on a not-found CEPHEADv0.2.0main
A CEP that fails is rarely a random number: it is usually someone who knows the city and guessed, most often the -000 that 957 municipalities never received. The API now answers 404 with an optional faixa naming the municipality that number would have belonged to, so the caller can say "you are in Areal, but not at that number" instead of a dead end. It stays an error. Cepis.Faixa hangs off Cepis.NotFoundError as context for the failure, never as an address, and parsing is deliberately tolerant: an absent, truncated or unreadable faixa yields nil rather than costing the caller a clean definitive answer. The bounds are the smallest and largest CEP that actually exist there, not the range the Correios publish, and almost nothing between them is valid — hence ceps and density/1, so nobody reads the pair as a list of numbers to try.
Diffstat (limited to 'lib/cepis.ex')
-rw-r--r--lib/cepis.ex50
1 files changed, 46 insertions, 4 deletions
diff --git a/lib/cepis.ex b/lib/cepis.ex
index 249e3f1..95d0579 100644
--- a/lib/cepis.ex
+++ b/lib/cepis.ex
@@ -27,7 +27,7 @@ defmodule Cepis do
the JSON is what you access on the struct. See `Cepis.Address`.
"""
- alias Cepis.{Address, Client, CSV, Health, HTTP}
+ alias Cepis.{Address, Client, CSV, Faixa, Health, HTTP}
alias Cepis.{InvalidCEPError, InvalidResponseError, NotFoundError}
@max_batch 500
@@ -93,11 +93,30 @@ defmodule Cepis do
message: the format already passed local validation, so the server knows
something this client does not.
+ ## When the CEP does not exist
+
+ A failing CEP is rarely random — it is usually someone who knows the city and
+ guessed the number, most often the `-000`, which 957 municipalities never
+ received. So `Cepis.NotFoundError` may carry a `Cepis.Faixa` naming the
+ municipality that number *would* have belonged to:
+
+ {:error, %Cepis.NotFoundError{faixa: faixa}} = Cepis.lookup("25845000")
+
+ if faixa do
+ # "Areal", "RJ" — the CEP is still not in the database
+ {faixa.localidade, faixa.uf}
+ end
+
+ It is still a not-found, and `faixa` is context for the failure rather than
+ an address. It is `nil` whenever the number falls in a band no municipality
+ owns.
+
## Errors
* `Cepis.InvalidCEPError` — not 8 digits (no request is made), or the API
rejected it.
- * `Cepis.NotFoundError` — well-formed, but not in the database.
+ * `Cepis.NotFoundError` — well-formed, but not in the database. May carry
+ the municipality in `faixa`.
* `Cepis.RateLimitError` — 429 after every attempt.
* `Cepis.ServerError` — 5xx after every attempt.
* `Cepis.NetworkError` — the request never completed.
@@ -197,11 +216,14 @@ defmodule Cepis do
@spec health!(Client.t() | keyword()) :: Health.t()
def health!(client \\ []), do: unwrap(health(client))
- defp from_response(404, _body, number) do
+ defp from_response(404, body, number) do
+ faixa = faixa(body)
+
{:error,
%NotFoundError{
cep: number,
- message: "CEP #{number} does not exist in the DNE database"
+ faixa: faixa,
+ message: not_found_message(number, faixa)
}}
end
@@ -213,6 +235,26 @@ defmodule Cepis do
with {:ok, data} <- decode(body), do: {:ok, Address.from_map(data)}
end
+ # A 404 body is a courtesy, not a contract: if it is absent, truncated or not
+ # JSON at all, the not-found still stands on its own. Nothing here may turn a
+ # clean definitive answer into a parse failure.
+ defp faixa(body) do
+ case decode(body) do
+ {:ok, data} -> Faixa.from_map(Map.get(data, "faixa"))
+ {:error, _reason} -> nil
+ end
+ end
+
+ defp not_found_message(number, nil) do
+ "CEP #{number} does not exist in the DNE database"
+ end
+
+ defp not_found_message(number, %Faixa{} = faixa) do
+ "CEP #{number} does not exist in the DNE database — the range belongs to " <>
+ "#{faixa.localidade}/#{faixa.uf}, which has #{faixa.ceps} CEPs between " <>
+ "#{faixa.cep_ini} and #{faixa.cep_fim}"
+ end
+
defp lookup_chunk(client, ceps) do
query = URI.encode_query(%{"ceps" => Enum.map_join(ceps, ",", &to_string/1)})