From eb15ec9b2715783fcd2bf4f834595d1bd549c09d Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:37:05 -0300 Subject: Report the municipality behind a not-found CEP A CEP that fails is rarely a random number. It is usually someone who knows the city and guessed the last digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers those 404s with a faixa block naming the municipality the CEP would have belonged to, and telling a caller "that CEP does not exist, did you mean somewhere in Areal/RJ?" is worth far more than telling them "no". It rides on cepis_error_t rather than on cepis_address_t because it is context for a failure, not an address: the lookup still returns CEPIS_ERR_NOT_FOUND and still hands back no address. Embedded by value with a has_faixa flag, so the error keeps owning nothing and keeps living on the stack with nothing to free; cepis_error_faixa gives the absent case the same NULL-or-not shape as cepis_address_t::geo, which is what the API's omit-never-null encoding calls for. Parsing is total on purpose. A body that is not JSON, a faixa that is null, a string, an array, or an object with no localidade all mean "the API named no municipality". A 404 is already the answer, and failing to read the garnish must never turn it into a different one. The bounds need reading with care, hence the length of the comments: cep_ini and cep_fim are the smallest and largest CEP that actually exist, not the range the Correios publish -- the DNE's own start for Areal is 25845000, exactly the CEP that just failed -- and the range is sparse, 118 CEPs scattered over the ~4900 numbers up to 25849899. That is what ceps is for. --- src/model.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) (limited to 'src/model.c') diff --git a/src/model.c b/src/model.c index 75ed0d4..2358b11 100644 --- a/src/model.c +++ b/src/model.c @@ -1,6 +1,7 @@ #include "internal.h" #include +#include #include #include @@ -113,6 +114,59 @@ bool cepis_health_ok(const cepis_health_t *health) strcmp(health->status, "ok") == 0; } +const cepis_faixa_t *cepis_error_faixa(const cepis_error_t *err) +{ + if (err == NULL || !err->has_faixa) + return NULL; + return &err->faixa; +} + +/* + * Copies at most `size - 1` bytes and always terminates. A municipality name + * longer than the buffer is truncated rather than rejected: the point of the + * block is to tell the caller which city they meant, and a truncated name + * still does that. + */ +static void copy_member(char *out, size_t size, const json_value_t *block, + const char *name) +{ + const char *text = json_as_string(json_object_get(block, name)); + + snprintf(out, size, "%s", text != NULL ? text : ""); +} + +bool cepis_faixa_from_json(const json_value_t *object, cepis_faixa_t *out) +{ + const json_value_t *block; + const char *localidade; + + if (out == NULL) + return false; + block = json_object_get(object, "faixa"); + if (block == NULL || block->type != JSON_OBJECT) + return false; + /* + * The municipality is the whole reason the block exists. Without it + * there is nothing to report, so an empty or non-string `localidade` + * counts as no block at all rather than as a nameless one. + */ + localidade = json_as_string(json_object_get(block, "localidade")); + if (localidade == NULL || localidade[0] == '\0') + return false; + + memset(out, 0, sizeof(*out)); + snprintf(out->localidade, sizeof(out->localidade), "%s", localidade); + copy_member(out->uf, sizeof(out->uf), block, "uf"); + copy_member(out->estado, sizeof(out->estado), block, "estado"); + copy_member(out->regiao, sizeof(out->regiao), block, "regiao"); + copy_member(out->ibge, sizeof(out->ibge), block, "ibge"); + copy_member(out->cep_ini, sizeof(out->cep_ini), block, "cep_ini"); + copy_member(out->cep_fim, sizeof(out->cep_fim), block, "cep_fim"); + if (!json_as_llong(json_object_get(block, "ceps"), &out->ceps)) + out->ceps = 0; + return true; +} + static cepis_geo_t *geo_from_json(const json_value_t *block) { const char *precisao; -- cgit v1.3