From 487b4c5628c02130580c675925a1d64b09a5f6cb Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:33:37 -0300 Subject: Carry the municipality of a CEP that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 404 from the API now brings a "faixa" naming the municipality the CEP would have belonged to, and NotFound carries it. A CEP that fails is rarely a random number: it is usually someone who knows the city and guessed the digits, so naming the city turns a dead end into an answer the caller can show. It stays an error. The range it carries is the CEPs that actually exist there, not the bounds the Correios publish — those start at the very CEP that just failed — and it is sparse, which is what "ceps" measures. The field is decoration on a failure, so parsing it is entirely defensive: absent, null or malformed all yield None rather than costing a clean not-found. Breaking: NotFound gained a field, hence 0.2.0. --- src/client.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) (limited to 'src/client.rs') diff --git a/src/client.rs b/src/client.rs index 8d73f06..4e6afcd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use serde_json::{Map, Value}; use crate::csv; use crate::error::{Error, Result}; -use crate::model::{Address, Health}; +use crate::model::{Address, Faixa, Health}; use crate::retry::{jitter, parse_retry_after}; /// Where the public API lives. @@ -270,7 +270,9 @@ impl Client { /// # Errors /// /// - [`Error::InvalidCep`] — not 8 digits (no request is made). - /// - [`Error::NotFound`] — well-formed, but not in the database. + /// - [`Error::NotFound`] — well-formed, but not in the database. It carries + /// a [`Faixa`] naming the municipality the CEP would belong to, whenever + /// the API can place it in one. /// - [`Error::RateLimited`] — 429 after every attempt. /// - [`Error::Server`] — 5xx after every attempt. /// - [`Error::Network`] — the request never completed. @@ -280,7 +282,10 @@ impl Client { let (status, body) = self.request(&format!("/{number}"), None, &[200])?; if status == 404 { - return Err(Error::NotFound { cep: number }); + return Err(Error::NotFound { + cep: number, + faixa: faixa(&body), + }); } if status == 400 { return Err(Error::invalid_cep(error_message( @@ -548,6 +553,22 @@ fn error_message(body: &[u8], fallback: &str) -> String { .unwrap_or_else(|| fallback.to_owned()) } +/// Pull the `faixa` block out of a 404 body, if there is a readable one. +/// +/// Every step is allowed to fail into `None`: a body that is not JSON, a +/// missing or null `faixa`, an object that names no municipality. The caller is +/// on its way to returning [`Error::NotFound`] either way, and an unparseable +/// extra field must not turn a correct not-found into an +/// [`Error::InvalidResponse`]. +fn faixa(body: &[u8]) -> Option> { + serde_json::from_slice::(body) + .ok() + .as_ref() + .and_then(|v| v.get("faixa")) + .and_then(Faixa::from_json) + .map(Box::new) +} + fn preview(body: &[u8]) -> String { let text = String::from_utf8_lossy(body); let cut = text -- cgit v1.3