From 2fb251996798798af38903fe535b8d147084a50a Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:33:51 -0300 Subject: Expose the municipality behind a not-found CEP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CEP that fails a lookup is rarely random: it is usually someone who knows the city and guessed the number, and the classic guess is the -000 that 957 municipalities never received. The API now names, on the 404 itself, the municipality that CEP would have belonged to, so a caller can say "not found — did you mean somewhere in Areal/RJ?" instead of just "not found". CepNotFound carries it as an optional Faixa. It stays an error: the block is context, not an address, and the range it describes is sparse, which is why it reports a count alongside the first and last CEP that actually exist there rather than the bounds the Correios publish. Parsing it is best-effort on purpose — a missing, empty or malformed block leaves the optional empty, since a 404 is already the answer and reading the hint attached to it must never cost the diagnosis. --- src/client.cpp | 21 ++++++++++++++++++++- src/models.cpp | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client.cpp b/src/client.cpp index ef19da8..84011a3 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -170,6 +170,20 @@ std::string message_from_error_body(const std::string& body, const std::string& return fallback; } +/// Reads the `faixa` block a 404 may carry, never failing. +/// +/// A body that is not JSON, or carries no `faixa`, or carries one that cannot +/// be read, all give `std::nullopt`. The 404 is the answer; the hint attached +/// to it is a bonus and must never cost the diagnosis. +std::optional faixa_from_error_body(const std::string& body) { + const std::optional parsed = Json::parse(body); + if (!parsed) { + return std::nullopt; + } + const Json* block = parsed->find("faixa"); + return block == nullptr ? std::nullopt : Faixa::from_json(*block); +} + std::string preview(const std::string& body) { constexpr std::size_t kLimit = 120; return body.size() <= kLimit ? body : body.substr(0, kLimit) + "..."; @@ -232,7 +246,12 @@ Address Client::lookup(std::string_view cep) const { const Response response = request("/" + number, {200}); if (response.status == 404) { - throw CepNotFound("CEP " + number + " does not exist in the DNE database"); + std::optional faixa = faixa_from_error_body(response.body); + std::string message = "CEP " + number + " does not exist in the DNE database"; + if (faixa) { + message += " (the range it falls in belongs to " + faixa->localidade + "/" + faixa->uf + ")"; + } + throw CepNotFound(message, std::move(faixa)); } if (response.status == 400) { throw InvalidCep(message_from_error_body(response.body, "CEP " + number + " rejected")); diff --git a/src/models.cpp b/src/models.cpp index 84e03f9..088283b 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -124,6 +124,25 @@ Address Address::from_json(const Json& data) { return address; } +std::optional Faixa::from_json(const Json& data) { + if (!data.is_object()) { + return std::nullopt; + } + Faixa faixa; + faixa.localidade = as_field(data.find("localidade")); + faixa.uf = as_field(data.find("uf")); + if (faixa.localidade.empty() || faixa.uf.empty()) { + return std::nullopt; + } + faixa.estado = as_field(data.find("estado")); + faixa.regiao = as_field(data.find("regiao")); + faixa.ibge = as_field(data.find("ibge")); + faixa.cep_ini = as_field(data.find("cep_ini")); + faixa.cep_fim = as_field(data.find("cep_fim")); + faixa.ceps = as_integer(data.find("ceps")).value_or(0); + return faixa; +} + Health Health::from_json(const Json& data) { Health health; health.status = as_field(data.find("status")); -- cgit v1.3