summaryrefslogtreecommitdiffstats
path: root/src/client.rs
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:33:37 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:33:37 -0300
commit487b4c5628c02130580c675925a1d64b09a5f6cb (patch)
tree240338af0c2c246bb2c689464f63d70b1f27f240 /src/client.rs
parent4a8c3b675df4b1a6b29904fc2af60605002a7f9e (diff)
downloadrust-eeaee4cece87d9dfe4ae0bea390f24a01a5f185b.tar.gz
Carry the municipality of a CEP that does not existHEADv0.2.0main
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.
Diffstat (limited to 'src/client.rs')
-rw-r--r--src/client.rs27
1 files changed, 24 insertions, 3 deletions
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<Box<Faixa>> {
+ serde_json::from_slice::<Value>(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