diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:37:09 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:37:09 -0300 |
| commit | a891657aa43d2aba06a6347dd2f94edd19afd6ba (patch) | |
| tree | b6369ed2c8485455f9c1aa707c1cd287cb8e7065 /source/cepis/model.d | |
| parent | 794f28199a4dde9d18ec711864336c96c58bb422 (diff) | |
| download | d-a891657aa43d2aba06a6347dd2f94edd19afd6ba.tar.gz | |
A 404 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 API now attaches a faixa to the
not-found body naming the municipality that owns the range, so the caller
can say "no such CEP, but that range is Areal/RJ" instead of only "no".
It stays an error. Faixa hangs off CepNotFoundException, never off
Address, and parsing is deliberately lenient: an absent, malformed or
unnamed block yields a null Nullable, so reading the extra context can
never cost the caller the answer it already had.
cep_ini and cep_fim are the smallest and largest CEP that actually exist
there, not the bounds the Correios publish, and the range between them is
sparse — hence ceps, so nobody mistakes the pair for a list.
Diffstat (limited to 'source/cepis/model.d')
| -rw-r--r-- | source/cepis/model.d | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/source/cepis/model.d b/source/cepis/model.d index 9ec3ffb..01692dd 100644 --- a/source/cepis/model.d +++ b/source/cepis/model.d @@ -201,6 +201,92 @@ struct Address } /** + * The municipality a CEP that does $(B not) exist would have belonged to. + * + * This arrives with a $(B 404), on $(D CepNotFoundException.faixa). It is + * $(B context for an error, never an address): the CEP asked for does not + * exist, and nothing here says otherwise. Rendering it as though the lookup had + * succeeded turns a clean not-found into a wrong answer. + * + * It is worth having because a CEP that fails is rarely a random number — it is + * someone who knows the city and guessed the digits, and the classic guess is + * the `-000` that 957 municipalities never received. + * + * $(D cep_ini) and $(D cep_fim) are the smallest and the largest CEP that + * $(B actually exist) in that municipality, $(B not) the bounds the Correios + * publish for it. The DNE's own start for Areal is `25845000` — precisely the + * CEP that just failed; the smallest one that exists is `25845001`. + * + * The range is also $(B sparse). Areal's 118 CEPs are scattered across the + * ~4,900 numbers between `25845001` and `25849899`. That is what $(D ceps) is + * for: never present the pair as the list of valid CEPs for the city, and never + * walk it expecting the numbers in between to resolve. + */ +struct Faixa +{ + /// Municipality that owns the range: `"Areal"`. + string localidade; + /// Federative unit, two letters: `"RJ"`. + string uf; + /// Federative unit spelled out: `"Rio de Janeiro"`. + string estado; + /// Region: `"Sudeste"`, `"Nordeste"`, and so on. + string regiao; + /// IBGE municipality code — the key to join with public datasets. + string ibge; + /// The smallest CEP that exists in the municipality, unmasked. + string cep_ini; + /// The largest CEP that exists in the municipality, unmasked. + string cep_fim; + /// How many CEPs actually exist between $(D cep_ini) and $(D cep_fim). + ulong ceps; + + /** + * The municipality the way one says it out loud: `"Areal/RJ"`. + * + * For a message to a human — it is what the not-found error puts in its + * own. Falls back to whichever of the two the API sent. + */ + string localidadeUf() const pure nothrow + { + if (localidade.length == 0) + return uf; + if (uf.length == 0) + return localidade; + return localidade ~ "/" ~ uf; + } + + /** + * Build from the `faixa` block a 404 carried. + * + * Returns a null $(D Nullable) for anything that does not name a + * municipality: absent, not an object, or with an empty $(D localidade). + * The API $(B omits) the block — it never sends null — when the CEP falls + * in a band no municipality owns, and that is an ordinary not-found, not a + * failure. Nothing read here may turn a 404 into a parse error. + */ + static Nullable!Faixa fromJSON(const JSONValue value) + { + if (value.type != JSONType.object) + return Nullable!Faixa.init; + + Faixa faixa; + faixa.localidade = jsonText(value, "localidade"); + if (faixa.localidade.length == 0) + return Nullable!Faixa.init; + + faixa.uf = jsonText(value, "uf"); + faixa.estado = jsonText(value, "estado"); + faixa.regiao = jsonText(value, "regiao"); + faixa.ibge = jsonText(value, "ibge"); + faixa.cep_ini = jsonText(value, "cep_ini"); + faixa.cep_fim = jsonText(value, "cep_fim"); + faixa.ceps = jsonCount(value, "ceps"); + return nullable(faixa); + } +} + +/** * The response of `GET /health`. * * Good for two things: checking that the API answers, and knowing $(B which @@ -365,6 +451,49 @@ unittest assert(address.raw["campo_do_futuro"].str == "value"); } +@("a faixa names the municipality the missing cep would belong to") +unittest +{ + import std.json : parseJSON; + + const faixa = Faixa.fromJSON(parseJSON(`{"localidade":"Areal","uf":"RJ",` + ~ `"estado":"Rio de Janeiro","regiao":"Sudeste","ibge":"3300225",` + ~ `"cep_ini":"25845001","cep_fim":"25849899","ceps":118}`)); + + assert(!faixa.isNull); + assert(faixa.get.localidade == "Areal"); + assert(faixa.get.ibge == "3300225"); + assert(faixa.get.cep_ini == "25845001"); + assert(faixa.get.cep_fim == "25849899"); + assert(faixa.get.ceps == 118); + assert(faixa.get.localidadeUf == "Areal/RJ"); +} + +@("a faixa that names no municipality is absent, not an error") +unittest +{ + import std.json : parseJSON; + + assert(Faixa.fromJSON(parseJSON(`{}`)).isNull); + assert(Faixa.fromJSON(parseJSON(`{"localidade":"","uf":"RJ"}`)).isNull); + assert(Faixa.fromJSON(parseJSON(`"Areal"`)).isNull); + assert(Faixa.fromJSON(parseJSON(`null`)).isNull); + assert(Faixa.fromJSON(JSONValue.init).isNull); +} + +@("a partial faixa keeps what it has") +unittest +{ + import std.json : parseJSON; + + const faixa = Faixa.fromJSON(parseJSON(`{"localidade":"Areal","ceps":"118"}`)); + + assert(!faixa.isNull); + assert(faixa.get.ceps == 118); + assert(faixa.get.cep_ini == ""); + assert(faixa.get.localidadeUf == "Areal"); +} + @("health reads counts as numbers") unittest { |