summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.c8
-rw-r--r--src/client.c29
-rw-r--r--src/internal.h15
-rw-r--r--src/model.c54
-rw-r--r--src/util.c6
5 files changed, 111 insertions, 1 deletions
diff --git a/src/cli.c b/src/cli.c
index e4c9bd4..009de33 100644
--- a/src/cli.c
+++ b/src/cli.c
@@ -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;
diff --git a/src/util.c b/src/util.c
index a2c6c18..ea36991 100644
--- a/src/util.c
+++ b/src/util.c
@@ -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);