summaryrefslogtreecommitdiffstats
path: root/source/cepis/client.d
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:37:09 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:37:09 -0300
commita891657aa43d2aba06a6347dd2f94edd19afd6ba (patch)
treeb6369ed2c8485455f9c1aa707c1cd287cb8e7065 /source/cepis/client.d
parent794f28199a4dde9d18ec711864336c96c58bb422 (diff)
downloadd-a891657aa43d2aba06a6347dd2f94edd19afd6ba.tar.gz
Carry the municipality a missing CEP would belong toHEADv0.2.0main
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/client.d')
-rw-r--r--source/cepis/client.d37
1 files changed, 34 insertions, 3 deletions
diff --git a/source/cepis/client.d b/source/cepis/client.d
index a93af7f..ef2f10e 100644
--- a/source/cepis/client.d
+++ b/source/cepis/client.d
@@ -37,7 +37,7 @@ import cepis.retry;
@safe:
/// The version of this library, as sent in the `User-Agent`.
-enum string VERSION = "0.1.0";
+enum string VERSION = "0.2.0";
/// Where the public API lives.
enum string DEFAULT_URL = "https://cep.is";
@@ -232,11 +232,16 @@ struct Client
* API's own message: the format already passed local validation, so the
* server knows something this client does not.
*
+ * A 404 may carry a `faixa` — the municipality the CEP would have belonged
+ * to — which lands on $(D CepNotFoundException.faixa). It is still a 404:
+ * the block is context for the error and never becomes an $(D Address).
+ *
* Throws:
* $(UL
* $(LI $(D InvalidCepException) — not 8 digits (no request is made).)
* $(LI $(D CepNotFoundException) — well-formed, but not in the
- * database.)
+ * database; carries $(D faixa) when the API named a
+ * municipality.)
* $(LI $(D RateLimitedException) — 429 after every attempt.)
* $(LI $(D ServerException) — 5xx after every attempt.)
* $(LI $(D NetworkException) — the request never completed.)
@@ -250,7 +255,7 @@ struct Client
const response = request("/" ~ number, [ushort(200)]);
if (response.status == 404)
- throw new CepNotFoundException(number);
+ throw new CepNotFoundException(number, notFoundFaixa(response.content));
if (response.status == 400)
throw new InvalidCepException(errorMessage(response.content,
"CEP " ~ number ~ " rejected"));
@@ -540,6 +545,32 @@ private string errorMessage(string content, string fallback)
return fallback;
}
+/**
+ * Pull the `faixa` block out of a 404 body.
+ *
+ * Every failure here — a body that is not JSON, a `faixa` that is missing,
+ * empty or malformed — is null, not an exception. The answer to the question
+ * asked was "this CEP does not exist"; failing to read the extra context the
+ * API attached to it must never cost the caller that answer.
+ */
+private Nullable!Faixa notFoundFaixa(string content)
+{
+ try
+ {
+ const value = parseJSON(content);
+ if (value.type == JSONType.object)
+ {
+ const object = value.objectNoRef;
+ if (auto block = "faixa" in object)
+ return Faixa.fromJSON(*block);
+ }
+ }
+ catch (Exception)
+ {
+ }
+ return Nullable!Faixa.init;
+}
+
private string preview(string content)
{
return `"` ~ sanitize(content).byDchar.take(120).array.to!string ~ `"`;