summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--README.md62
-rw-r--r--include/cepis.h105
-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
-rw-r--r--tests/test_cepis.c124
9 files changed, 399 insertions, 8 deletions
diff --git a/Makefile b/Makefile
index c7ff2ba..0728192 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
-VERSION := 0.1.0
-SOVERSION := 0
+VERSION := 0.2.0
+SOVERSION := 1
CC ?= cc
AR ?= ar
diff --git a/README.md b/README.md
index 2c47a6e..c49e25c 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,13 @@ make build
sudo make install # PREFIX=/usr/local by default
```
+> **0.2.0 changed `sizeof(cepis_error_t)`** — it grew a `faixa` member. The
+> source API is unchanged and existing code compiles untouched, but a program
+> built against 0.1.0 must be **recompiled**, not just relinked: it declares
+> the struct on its own stack at the old size, and the new library would write
+> past the end of it. The soname went from `libcepis.so.0` to `libcepis.so.1`
+> so the loader refuses that pairing instead of corrupting it silently.
+
Then compile against it:
```sh
@@ -191,7 +198,7 @@ config.max_attempts = 3; /* counting the first; 1 disables retry */
config.backoff_base = 0.5; /* base of the exponential wait */
config.backoff_max = 8.0; /* cap on each wait */
config.total_timeout = 0.0; /* cap on the whole call; 0 = none */
-config.user_agent = NULL; /* NULL uses cepis-c/0.1.0 */
+config.user_agent = NULL; /* NULL uses cepis-c/0.2.0 */
cepis_client_new(&config, &client, &err);
```
@@ -251,6 +258,59 @@ default:
API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an
HTML error page. Useful as a diagnosis: the problem is not in your code.
+### A CEP that does not exist
+
+A `CEPIS_ERR_NOT_FOUND` often knows more than "no". 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. So the error can name the municipality the CEP *would* have belonged
+to:
+
+```c
+const cepis_faixa_t *faixa;
+
+/* Still CEPIS_ERR_NOT_FOUND, still a 404. */
+cepis_lookup(client, "25845-000", &address, &err);
+
+faixa = cepis_error_faixa(&err);
+if (faixa != NULL) {
+ faixa->localidade; /* "Areal" */
+ faixa->uf; /* "RJ" */
+ faixa->estado; /* "Rio de Janeiro" */
+ faixa->regiao; /* "Sudeste" */
+ faixa->ibge; /* "3300225" */
+ faixa->cep_ini; /* "25845001" */
+ faixa->cep_fim; /* "25849899" */
+ faixa->ceps; /* 118 */
+}
+```
+
+Three things this is not:
+
+- **Not an address.** The CEP asked for still does not exist and the call
+ still failed. Never promote `faixa` to a delivery address, and never let it
+ turn a not-found into a success. Use it to say "that CEP does not exist —
+ did you mean somewhere in Areal/RJ?".
+- **Not the range the Correios publish.** `cep_ini` and `cep_fim` are the
+ smallest and largest CEP that *actually exist* there. The DNE's own start
+ for Areal is `25845000` — exactly the CEP that just failed. `cep_ini` is
+ `25845001`, the first one really in the database.
+- **Not a list.** The range is sparse: Areal's 118 CEPs are scattered across
+ the ~4900 numbers between `25845001` and `25849899`. Iterating the interval
+ mostly produces CEPs that do not exist either. That is what `ceps` is for —
+ say "118 CEPs in Areal", never "4899".
+
+`faixa` is **omitted, never null**, when the CEP falls in a band no
+municipality owns, so `cepis_error_faixa` returning `NULL` — equivalently
+`err.has_faixa == false` — is how you tell absent from present. A missing,
+malformed or empty block leaves it absent and never changes the status: a 404
+is already the answer, and failing to read the garnish must not turn it into a
+different one.
+
+`cepis_error_t` still owns nothing and still lives on the stack: `faixa` is
+embedded by value, so there is nothing extra to free. The pointer
+`cepis_error_faixa` returns is borrowed from the error and dies with it.
+
## Development
```sh
diff --git a/include/cepis.h b/include/cepis.h
index 6b2dc9a..d639d62 100644
--- a/include/cepis.h
+++ b/include/cepis.h
@@ -39,6 +39,10 @@
* borrowed for the duration of the call only; the library copies whatever it
* needs to keep.
*
+ * A ::cepis_error_t is the exception in the other direction: it owns nothing,
+ * lives happily on the stack and is never freed. ::cepis_error_faixa therefore
+ * returns a pointer *into* it, borrowed for as long as the error itself lives.
+ *
* @section fieldnames Field names
*
* The API surface is English, but the **data fields keep the API's own
@@ -63,7 +67,7 @@ extern "C" {
#endif
/** Library version. */
-#define CEPIS_VERSION "0.1.0"
+#define CEPIS_VERSION "0.2.0"
/** Default API endpoint. */
#define CEPIS_DEFAULT_BASE_URL "https://cep.is"
@@ -77,6 +81,12 @@ extern "C" {
/** Size of ::cepis_error_t::message, terminator included. */
#define CEPIS_ERROR_MESSAGE_MAX 256
+/** Size of the longer ::cepis_faixa_t members, terminator included. */
+#define CEPIS_FAIXA_NAME_MAX 80
+
+/** Size of the short coded ::cepis_faixa_t members, terminator included. */
+#define CEPIS_FAIXA_CODE_MAX 16
+
/**
* @brief Result of every fallible call.
*
@@ -108,11 +118,58 @@ typedef enum cepis_status {
} cepis_status_t;
/**
+ * @brief The municipality a CEP that does not exist would have belonged to.
+ *
+ * Context for a ::CEPIS_ERR_NOT_FOUND, **not an address**. The CEP asked for
+ * still does not exist and the lookup still failed with a 404; this only names
+ * the band it fell inside. Never promote it to a delivery address, and never
+ * let it turn a not-found into a success.
+ *
+ * It exists because 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.
+ *
+ * @section faixabounds What the bounds are, and are not
+ *
+ * ::cep_ini and ::cep_fim are the smallest and the largest CEP that **actually
+ * exist** in that municipality -- not the range the Correios publish for it.
+ * For Areal/RJ the DNE's own start is `25845000`, which is exactly the CEP that
+ * just failed; ::cep_ini is `25845001`, the first one really there.
+ *
+ * The range is **sparse**. Areal's ::ceps is 118, spread over the ~4900 numbers
+ * between `25845001` and `25849899`. Iterating the interval, or offering the
+ * pair as a list of valid CEPs, produces mostly CEPs that do not exist either.
+ * ::ceps is there precisely so that cannot pass unnoticed: use it to say "118
+ * CEPs in Areal", never "4899 CEPs".
+ */
+typedef struct cepis_faixa {
+ /** Municipality name. Never `NULL`, never empty when present. */
+ char localidade[CEPIS_FAIXA_NAME_MAX];
+ /** Two-letter state code, e.g. `"RJ"`. */
+ char uf[CEPIS_FAIXA_CODE_MAX];
+ char estado[CEPIS_FAIXA_NAME_MAX];
+ char regiao[CEPIS_FAIXA_CODE_MAX];
+ /** IBGE municipality code. */
+ char ibge[CEPIS_FAIXA_CODE_MAX];
+ /** Smallest CEP that exists there, unmasked. */
+ char cep_ini[CEPIS_CEP_LEN + 1];
+ /** Largest CEP that exists there, unmasked. */
+ char cep_fim[CEPIS_CEP_LEN + 1];
+ /**
+ * How many CEPs exist between ::cep_ini and ::cep_fim. Far smaller
+ * than the numeric distance between them. `0` when the API did not
+ * say.
+ */
+ long long ceps;
+} cepis_faixa_t;
+
+/**
* @brief Detail about a failure.
*
- * Owns no memory: the message lives in a fixed buffer inside the struct, so an
- * error can be declared on the stack and forgotten. Pass a pointer to one as
- * the last argument of any fallible call, or `NULL` to discard the detail.
+ * Owns no memory: the message and ::faixa live in fixed buffers inside the
+ * struct, so an error can be declared on the stack and forgotten. Pass a
+ * pointer to one as the last argument of any fallible call, or `NULL` to
+ * discard the detail.
*/
typedef struct cepis_error {
/** Same value the call returned. */
@@ -128,6 +185,19 @@ typedef struct cepis_error {
* attempts it made; it is exposed for rescheduling the work.
*/
double retry_after;
+ /**
+ * `true` when ::faixa was filled. Only ::CEPIS_ERR_NOT_FOUND from
+ * ::cepis_lookup ever sets it, and only when the CEP fell inside a
+ * band some municipality owns -- the API **omits** the block, rather
+ * than sending null, for a band nobody owns. Always test this before
+ * reading ::faixa, or use ::cepis_error_faixa.
+ */
+ bool has_faixa;
+ /**
+ * The municipality the missing CEP would have belonged to. Meaningful
+ * only while ::has_faixa is `true`; untouched otherwise.
+ */
+ cepis_faixa_t faixa;
} cepis_error_t;
/**
@@ -345,6 +415,11 @@ const char *cepis_client_user_agent(const cepis_client_t *client);
* own message: the format already passed local validation, so the server knows
* something this client does not.
*
+ * On ::CEPIS_ERR_NOT_FOUND the API often names the municipality the CEP would
+ * have belonged to; it arrives in ::cepis_error_t::faixa, guarded by
+ * ::cepis_error_t::has_faixa. It is context for the failure and never an
+ * address -- see ::cepis_faixa_t.
+ *
* This is the only call that carries ::cepis_address_t::geo.
*/
cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep,
@@ -458,6 +533,28 @@ bool cepis_geo_is_precise(const cepis_geo_t *geo);
/** @brief `true` when the API reports itself healthy. `false` for `NULL`. */
bool cepis_health_ok(const cepis_health_t *health);
+/**
+ * @brief The municipality named by a not-found error, or `NULL`.
+ *
+ * @return A pointer borrowed from @p err, valid as long as @p err is, or
+ * `NULL` when the API named no municipality -- for `NULL`, for any
+ * status other than ::CEPIS_ERR_NOT_FOUND, and for a CEP that fell in
+ * a band nobody owns.
+ *
+ * Reads better than testing ::cepis_error_t::has_faixa by hand and gives the
+ * absent case the same shape as ::cepis_address_t::geo:
+ *
+ * @code
+ * const cepis_faixa_t *faixa = cepis_error_faixa(&err);
+ *
+ * if (faixa != NULL)
+ * printf("no such CEP; %s/%s has %lld, from %s to %s\n",
+ * faixa->localidade, faixa->uf, faixa->ceps, faixa->cep_ini,
+ * faixa->cep_fim);
+ * @endcode
+ */
+const cepis_faixa_t *cepis_error_faixa(const cepis_error_t *err);
+
/** @brief A short, stable, English name for a status code. */
const char *cepis_status_str(cepis_status_t status);
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);
diff --git a/tests/test_cepis.c b/tests/test_cepis.c
index ee23305..b17ab67 100644
--- a/tests/test_cepis.c
+++ b/tests/test_cepis.c
@@ -310,6 +310,126 @@ static void test_not_found_is_not_retried(void)
fixture_down(&fixture);
}
+static void test_not_found_names_the_municipality(void)
+{
+ fixture_t fixture;
+ cepis_address_t *address = NULL;
+ cepis_error_t err;
+ const cepis_faixa_t *faixa;
+
+ fixture_up(&fixture);
+ ts_respond(fixture.server, "/25845000", 404,
+ "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":"
+ "\"CEP n\xC3\xA3o existe na base do DNE\",\"faixa\":"
+ "{\"localidade\":\"Areal\",\"uf\":\"RJ\",\"estado\":"
+ "\"Rio de Janeiro\",\"regiao\":\"Sudeste\",\"ibge\":"
+ "\"3300225\",\"cep_ini\":\"25845001\",\"cep_fim\":"
+ "\"25849899\",\"ceps\":118}}",
+ NULL);
+
+ /* Still a 404: the block is context for the failure, not an address. */
+ CHECK(cepis_lookup(fixture.client, "25845-000", &address, &err) ==
+ CEPIS_ERR_NOT_FOUND);
+ CHECK(address == NULL);
+ CHECK(err.http_status == 404);
+
+ CHECK(err.has_faixa);
+ faixa = cepis_error_faixa(&err);
+ CHECK(faixa == &err.faixa);
+ if (faixa != NULL) {
+ CHECK_STR(faixa->localidade, "Areal");
+ CHECK_STR(faixa->uf, "RJ");
+ CHECK_STR(faixa->estado, "Rio de Janeiro");
+ CHECK_STR(faixa->regiao, "Sudeste");
+ CHECK_STR(faixa->ibge, "3300225");
+ /*
+ * Not the DNE's own start, which is 25845000 -- the CEP that
+ * just failed. These are the smallest and largest that really
+ * exist, and the 118 are sparse across the ~4900 between them.
+ */
+ CHECK_STR(faixa->cep_ini, "25845001");
+ CHECK_STR(faixa->cep_fim, "25849899");
+ CHECK(faixa->ceps == 118);
+ }
+ fixture_down(&fixture);
+}
+
+static void test_not_found_without_faixa_leaves_it_absent(void)
+{
+ fixture_t fixture;
+ cepis_address_t *address = NULL;
+ cepis_error_t err;
+
+ fixture_up(&fixture);
+ ts_respond(fixture.server, "/99999999", 404,
+ "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":"
+ "\"CEP n\xC3\xA3o existe na base do DNE\"}",
+ NULL);
+
+ /* A band no municipality owns: the API omits the block, never nulls
+ * it, and the caller must be able to tell absent from present. */
+ CHECK(cepis_lookup(fixture.client, "99999999", &address, &err) ==
+ CEPIS_ERR_NOT_FOUND);
+ CHECK(!err.has_faixa);
+ CHECK(cepis_error_faixa(&err) == NULL);
+ fixture_down(&fixture);
+}
+
+static void test_malformed_faixa_is_still_a_clean_not_found(void)
+{
+ static const char *bodies[] = {
+ "{\"erro\":\"x\",\"faixa\":null}",
+ "{\"erro\":\"x\",\"faixa\":\"Areal\"}",
+ "{\"erro\":\"x\",\"faixa\":[]}",
+ "{\"erro\":\"x\",\"faixa\":{}}",
+ "{\"erro\":\"x\",\"faixa\":{\"localidade\":\"\"}}",
+ "{\"erro\":\"x\",\"faixa\":{\"uf\":\"RJ\",\"ceps\":118}}",
+ "{\"erro\":\"x\",\"faixa\":{\"localidade\":42}}",
+ "not json at all",
+ ""};
+ fixture_t fixture;
+ cepis_error_t err;
+ size_t i;
+
+ fixture_up(&fixture);
+ for (i = 0; i < sizeof(bodies) / sizeof(bodies[0]); i++) {
+ cepis_address_t *address = NULL;
+
+ ts_respond(fixture.server, "/99999999", 404, bodies[i], NULL);
+ CHECK(cepis_lookup(fixture.client, "99999999", &address,
+ &err) == CEPIS_ERR_NOT_FOUND);
+ CHECK(address == NULL);
+ CHECK(!err.has_faixa);
+ CHECK(cepis_error_faixa(&err) == NULL);
+ }
+ fixture_down(&fixture);
+}
+
+static void test_faixa_does_not_survive_the_next_error(void)
+{
+ fixture_t fixture;
+ cepis_address_t *address = NULL;
+ cepis_error_t err;
+
+ fixture_up(&fixture);
+ ts_respond(fixture.server, "/25845000", 404,
+ "{\"erro\":\"cep_nao_encontrado\",\"faixa\":"
+ "{\"localidade\":\"Areal\",\"uf\":\"RJ\",\"cep_ini\":"
+ "\"25845001\",\"cep_fim\":\"25849899\",\"ceps\":118}}",
+ NULL);
+ CHECK(cepis_lookup(fixture.client, "25845000", &address, &err) ==
+ CEPIS_ERR_NOT_FOUND);
+ CHECK(err.has_faixa);
+
+ /* Same error struct reused: a stale block would be read as a
+ * municipality this second failure never named. */
+ CHECK(cepis_lookup(fixture.client, "123", &address, &err) ==
+ CEPIS_ERR_INVALID_CEP);
+ CHECK(!err.has_faixa);
+ CHECK(cepis_error_faixa(&err) == NULL);
+ fixture_down(&fixture);
+}
+
static void test_invalid_cep_never_leaves_the_machine(void)
{
fixture_t fixture;
@@ -903,6 +1023,10 @@ int main(void)
RUN(test_malformed_geo_does_not_lose_the_address);
RUN(test_new_api_field_does_not_break);
RUN(test_not_found_is_not_retried);
+ RUN(test_not_found_names_the_municipality);
+ RUN(test_not_found_without_faixa_leaves_it_absent);
+ RUN(test_malformed_faixa_is_still_a_clean_not_found);
+ RUN(test_faixa_does_not_survive_the_next_error);
RUN(test_invalid_cep_never_leaves_the_machine);
RUN(test_api_400_becomes_invalid_cep);
RUN(test_response_that_is_not_json);