diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:37:05 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:44:20 -0300 |
| commit | eb15ec9b2715783fcd2bf4f834595d1bd549c09d (patch) | |
| tree | cd9c6e80892b409db346fde3fe8334f3c37a1130 /src | |
| parent | e2c6ae13af335b311da2429c903032124f926df2 (diff) | |
| download | c-a90051dad5efd24e867396ad840afa4d01a4d3ef.tar.gz | |
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.
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.c | 8 | ||||
| -rw-r--r-- | src/client.c | 29 | ||||
| -rw-r--r-- | src/internal.h | 15 | ||||
| -rw-r--r-- | src/model.c | 54 | ||||
| -rw-r--r-- | src/util.c | 6 |
5 files changed, 111 insertions, 1 deletions
@@ -157,8 +157,16 @@ int main(int argc, char **argv) if (count == 1) { cepis_address_t *address = NULL; if (cepis_lookup(client, ceps[0], &address, &err) != CEPIS_OK) { + const cepis_faixa_t *faixa = cepis_error_faixa(&err); fprintf(stderr, "error: %s: %s\n", cepis_status_str(err.status), err.message); + if (faixa != NULL) + fprintf(stderr, + "note: it would be in %s/%s, where " + "%lld CEP(s) exist between %s and %s\n", + faixa->localidade, faixa->uf, + faixa->ceps, faixa->cep_ini, + faixa->cep_fim); status = 1; } else { print_address(address); diff --git a/src/client.c b/src/client.c index 58c55b3..27812f2 100644 --- a/src/client.c +++ b/src/client.c @@ -513,6 +513,20 @@ static void api_message(const cepis_buf_t *body, char *out, size_t out_size, json_free(root); } +/* + * A 404 body may carry a `faixa` block naming the municipality the CEP would + * have belonged to. Nothing here can fail: an unparseable body just means the + * API named no municipality, which is also what an absent block means. + */ +static bool faixa_from_body(const cepis_buf_t *body, cepis_faixa_t *out) +{ + json_value_t *root = json_parse(body->data, body->len); + bool named = cepis_faixa_from_json(root, out); + + json_free(root); + return named; +} + static cepis_status_t build_url(const cepis_client_t *client, const char *path, cepis_buf_t *out) { @@ -558,10 +572,23 @@ cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep, return status; if (response.status == 404) { + cepis_faixa_t faixa; + bool named = faixa_from_body(&response.body, &faixa); + cepis_buf_free(&response.body); - return cepis_set_error( + status = cepis_set_error( err, CEPIS_ERR_NOT_FOUND, 404, -1.0, "CEP %s does not exist in the DNE database", number); + /* + * After cepis_set_error, which clears has_faixa: the block is + * extra detail about the failure, never a reason to report a + * different one. + */ + if (named && err != NULL) { + err->faixa = faixa; + err->has_faixa = true; + } + return status; } if (response.status == 400) { snprintf(fallback, sizeof(fallback), "CEP %s rejected", number); diff --git a/src/internal.h b/src/internal.h index 57487c2..700b1ee 100644 --- a/src/internal.h +++ b/src/internal.h @@ -71,4 +71,19 @@ cepis_address_t *cepis_address_from_csv(const csv_row_t *header, /** @brief Build a health report from a parsed JSON object. */ cepis_health_t *cepis_health_from_json(const json_value_t *object); +/** + * @brief Read the `faixa` block of a 404 body into @p out. + * @param object The decoded body, which may be `NULL`. + * @param out Filled and returning `true`, or untouched and returning + * `false`. + * @return `true` only when the block is present, is an object and names a + * municipality. + * + * Deliberately total: a body that is not JSON, a `faixa` that is null, a + * string, an array or an object without `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. + */ +bool cepis_faixa_from_json(const json_value_t *object, cepis_faixa_t *out); + #endif 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 <stddef.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> @@ -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; @@ -71,6 +71,12 @@ cepis_status_t cepis_set_error(cepis_error_t *err, cepis_status_t status, err->status = status; err->http_status = http_status; err->retry_after = retry_after; + /* + * Cleared here, not by the caller: an error is declared uninitialised + * on the stack and reused across calls, so anything left set would be + * read as a municipality that this failure never named. + */ + err->has_faixa = false; va_start(args, fmt); vsnprintf(err->message, sizeof(err->message), fmt, args); va_end(args); |