diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:37 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:37 -0300 |
| commit | 487b4c5628c02130580c675925a1d64b09a5f6cb (patch) | |
| tree | 240338af0c2c246bb2c689464f63d70b1f27f240 /src/model.rs | |
| parent | 4a8c3b675df4b1a6b29904fc2af60605002a7f9e (diff) | |
| download | rust-eeaee4cece87d9dfe4ae0bea390f24a01a5f185b.tar.gz | |
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/model.rs')
| -rw-r--r-- | src/model.rs | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/src/model.rs b/src/model.rs index 59cacbe..9092239 100644 --- a/src/model.rs +++ b/src/model.rs @@ -64,6 +64,87 @@ impl Geo { } } +/// The municipality a CEP that does not exist would have belonged to. +/// +/// This is **context for a not-found**, never an address. It rides on +/// [`Error::NotFound`](crate::Error::NotFound), the response is still a 404, and +/// nothing here says the CEP that was looked up exists — it does not. +/// +/// It is there because a CEP that fails is rarely a random number: it is +/// usually someone who knows the city and guessed the digits, and the classic +/// guess is the `-000` that 957 municipalities never received. +/// +/// # The range is not the Correios range +/// +/// [`cep_ini`](Faixa::cep_ini) and [`cep_fim`](Faixa::cep_fim) are the smallest +/// and the largest CEP that **actually exist** in that municipality, not the +/// bounds the Correios publish for it. For Areal/RJ the published start is +/// `25845000` — which is precisely the CEP that just failed. Showing the +/// official bounds would therefore suggest the CEP is valid. +/// +/// # The range is sparse +/// +/// The two ends are not a span of usable numbers. Areal's 118 CEPs are spread +/// over the roughly 4,900 numbers between `25845001` and `25849899`; the vast +/// majority of that interval does not exist either. That is what +/// [`ceps`](Faixa::ceps) is for — never present the pair as a list, or as a +/// count, of valid CEPs. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct Faixa { + /// Municipality. + pub localidade: String, + /// Federative unit, two letters: `"RJ"`. + pub uf: String, + /// Federative unit spelled out: `"Rio de Janeiro"`. + pub estado: String, + /// Region: `"Sudeste"`, `"Nordeste"`, and so on. + pub regiao: String, + /// IBGE municipality code — the key to join with public datasets. + pub ibge: String, + /// Smallest CEP that exists in the municipality, unmasked. + pub cep_ini: String, + /// Largest CEP that exists in the municipality, unmasked. + pub cep_fim: String, + /// How many CEPs exist between those two ends, the ends included. + /// + /// Read next to [`cep_ini`](Faixa::cep_ini) and [`cep_fim`](Faixa::cep_fim) + /// it is the measure of how sparse the interval is: 118 across ~4,900 + /// numbers for Areal/RJ. + pub ceps: u64, +} + +impl Faixa { + /// `"Areal/RJ"` — the municipality in the shortest form worth showing. + pub fn municipality(&self) -> String { + format!("{}/{}", self.localidade, self.uf) + } + + /// Build from the `faixa` block of a 404 body, or `None`. + /// + /// Anything short of an object naming a municipality yields `None`: + /// absent, null, empty, or malformed. The caller is already returning + /// [`Error::NotFound`](crate::Error::NotFound) — turning a clean not-found + /// into a parse failure over decoration would be a bad trade. + pub(crate) fn from_json(value: &Value) -> Option<Faixa> { + value.as_object()?; + let localidade = text(value, "localidade"); + if localidade.is_empty() { + return None; + } + Some(Faixa { + localidade, + uf: text(value, "uf"), + estado: text(value, "estado"), + regiao: text(value, "regiao"), + ibge: text(value, "ibge"), + cep_ini: text(value, "cep_ini"), + cep_fim: text(value, "cep_fim"), + ceps: count(value, "ceps"), + }) + } +} + /// A CEP and everything the database knows about it. /// /// Fields the database does not have for that CEP come back as empty strings, |