summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
commite2c6ae13af335b311da2429c903032124f926df2 (patch)
tree73776fab7f3e52124a217e2768968fc9a8a76c34 /README.md
downloadc-e2c6ae13af335b311da2429c903032124f926df2.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md273
1 files changed, 273 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2c47a6e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,273 @@
+# cepis
+
+C client for the [public CEP API at cep.is](https://cep.is). Official Correios
+data (DNE), refreshed every two weeks.
+
+```c
+#include <cepis.h>
+#include <stdio.h>
+
+int main(void)
+{
+ cepis_config_t config;
+ cepis_client_t *client;
+ cepis_address_t *address;
+ cepis_error_t err;
+
+ cepis_config_init(&config);
+ if (cepis_client_new(&config, &client, &err) != CEPIS_OK)
+ return 1;
+
+ if (cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK) {
+ printf("%s %s %s\n", address->logradouro, address->localidade,
+ address->uf);
+ cepis_address_free(address);
+ }
+
+ cepis_client_free(client);
+ return 0;
+}
+```
+
+## Installation
+
+Requires a C11 compiler and [libcurl](https://curl.se/libcurl/) with its
+headers (`libcurl-dev` / `libcurl-devel` / `curl`).
+
+```sh
+git clone https://git.cep.is/c.git cepis-c
+cd cepis-c
+make build
+sudo make install # PREFIX=/usr/local by default
+```
+
+Then compile against it:
+
+```sh
+cc myprogram.c -lcepis -lcurl -lm -o myprogram
+```
+
+Two dependencies, both deliberate: **libcurl**, because C has no standard HTTP
+client and libcurl is the one every platform already ships; and no JSON or CSV
+library at all — both parsers are hand-written here (`src/json.c`, `src/csv.c`,
+about 700 lines together) because the response shapes are small and fixed, and
+vendoring a general-purpose parser would add more code than it removes.
+
+## Usage
+
+### A single lookup
+
+`cepis_lookup` accepts the CEP masked or unmasked and fills a
+`cepis_address_t *` that you release with `cepis_address_free`:
+
+```c
+cepis_address_t *a;
+
+cepis_lookup(client, "01310-100", &a, &err);
+
+a->logradouro; /* "Avenida Paulista" */
+a->localidade; /* "São Paulo" */
+a->uf; /* "SP" */
+a->ibge; /* "3550308" */
+a->cep; /* "01310100" (always unmasked) */
+```
+
+Fields the database does not have for that CEP come back as the empty string,
+never `NULL`. For the masked form:
+
+```c
+char masked[CEPIS_CEP_LEN + 2];
+
+cepis_format_cep(a->cep, masked); /* "01310-100" */
+```
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`:
+
+```c
+if (a->geo != NULL) {
+ printf("%f %f\n", a->geo->lat, a->geo->lon); /* -23.567 -46.649 */
+ printf("%ld\n", a->geo->raio_m); /* 1084 */
+ printf("%s\n", a->geo->precisao); /* "logradouro" */
+}
+```
+
+**Check `precisao` before computing distance.** There are two kinds of point,
+and the difference between them is tens of kilometres:
+
+| `precisao` | typical radius | what the point is |
+| --- | --- | --- |
+| `logradouro` | 110–140 m | the stretch of street — good for maps and distance |
+| `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city |
+
+`cepis_geo_is_precise()` is the shorthand for `precisao == "logradouro"`:
+
+```c
+if (cepis_geo_is_precise(a->geo))
+ cost = shipping_from(a->geo->lat, a->geo->lon);
+else
+ cost = shipping_from_municipality(a->ibge);
+```
+
+### Many CEPs at once
+
+Do not loop over `cepis_lookup`. Use `cepis_lookup_many`, which resolves up to
+500 CEPs per request:
+
+```c
+const char *ceps[] = {"01001000", "99999999", "20040020"};
+cepis_address_list_t list;
+size_t i;
+
+if (cepis_lookup_many(client, ceps, 3, &list, &err) == CEPIS_OK) {
+ for (i = 0; i < list.count; i++) {
+ if (list.items[i] == NULL)
+ printf("%s: not found\n", ceps[i]);
+ else
+ printf("%s: %s\n", ceps[i], list.items[i]->localidade);
+ }
+ cepis_address_list_free(&list);
+}
+```
+
+The list has **the same length and the same order** as the input. A CEP that
+does not exist, or is malformed, is `NULL` at its position — it does not
+vanish, and it is not an error.
+
+Inputs longer than 500 are chunked automatically.
+
+> **Batches carry no coordinate.** The endpoint behind them is CSV, and the
+> CSV has no `geo` column. Every address from `cepis_lookup_many` has
+> `geo == NULL`, including for CEPs that do have one. Need `geo`? Use
+> `cepis_lookup`, one at a time.
+
+### API health
+
+Useful for two things: checking the API answers, and knowing **which version
+of the database** is live:
+
+```c
+cepis_health_t *h;
+
+cepis_health(client, &h, &err);
+cepis_health_ok(h); /* true */
+h->dne; /* "26071" identifier of the fortnightly database */
+h->ceps; /* 1596168 */
+h->base_gerada_em; /* "2026-07-24T03:48:28Z" */
+cepis_health_free(h);
+```
+
+An API reporting itself degraded is not an error — that arrives in `h->status`.
+
+### Memory
+
+Every function that hands back a pointer hands back ownership of it:
+
+| from | released by |
+| --- | --- |
+| `cepis_client_new` | `cepis_client_free` |
+| `cepis_lookup` | `cepis_address_free` |
+| `cepis_lookup_many` | `cepis_address_list_free` |
+| `cepis_health` | `cepis_health_free` |
+
+Every `char *` inside those structs is owned by the struct and freed with it;
+copy the bytes if you need them longer. Strings passed *into* the library are
+borrowed for the call only. A `cepis_error_t` owns nothing and can live on the
+stack.
+
+## Configuration
+
+Fill a `cepis_config_t` with `cepis_config_init` and override what you need.
+Reuse one client per thread; a client is not safe to share between threads:
+
+```c
+cepis_config_t config;
+
+cepis_config_init(&config);
+config.base_url = "https://cep.is"; /* point at another instance */
+config.timeout = 10.0; /* seconds per socket operation */
+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 */
+
+cepis_client_new(&config, &client, &err);
+```
+
+Retries cover 429, 5xx and network failures, with exponential backoff and
+jitter; 400 and 404 are never retried. `timeout` caps the connect phase and
+aborts a transfer that stalls for that long — it is not a cap on the whole
+call, which is what `total_timeout` is for. See `include/cepis.h` for the
+details of each member.
+
+### Pointing at a local instance
+
+`base_url` is the only member that usually changes between environments. The
+API repo brings a local instance up with `just up`:
+
+```c
+config.base_url = "http://localhost:8080";
+```
+
+From the command line, to confirm quickly that it answers:
+
+```sh
+cepis --base-url http://localhost:8080 01001000
+```
+
+## Errors
+
+Every fallible call returns a `cepis_status_t` and fills an optional
+`cepis_error_t` with the detail. The split is by **the decision you have to
+make**, not by the HTTP status that caused it:
+
+| status | when | does retrying help? |
+| --- | --- | --- |
+| `CEPIS_ERR_INVALID_CEP` | not 8 digits, or a 400 from the API | no |
+| `CEPIS_ERR_NOT_FOUND` | well-formed, not in the database | no |
+| `CEPIS_ERR_RATE_LIMITED` | 429; see `err.retry_after` | yes, later |
+| `CEPIS_ERR_SERVER` | 5xx; see `err.http_status` | yes, later |
+| `CEPIS_ERR_NETWORK` | DNS, connection, timeout, TLS | yes, later |
+| `CEPIS_ERR_INVALID_RESPONSE` | unintelligible response | probably not |
+| `CEPIS_ERR_CONFIG` | a configuration value out of range | no |
+| `CEPIS_ERR_NOMEM` | allocation failed | no |
+
+```c
+switch (cepis_lookup(client, cep_from_form, &address, &err)) {
+case CEPIS_OK:
+ break;
+case CEPIS_ERR_NOT_FOUND:
+ /* ask the user for another CEP */
+ break;
+default:
+ /* transient failure: reschedule */
+ fprintf(stderr, "%s: %s\n", cepis_status_str(err.status), err.message);
+}
+```
+
+`CEPIS_ERR_INVALID_RESPONSE` almost always means something between you and the
+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.
+
+## Development
+
+```sh
+make # list the targets
+make build # static and shared libraries, plus the cepis CLI
+make test # run the suite against a local HTTP server
+make lint # clang-format --dry-run + clang-tidy, touches nothing
+make format # apply clang-format (writes)
+make check # lint, then test, then build
+make memcheck # run the suite under valgrind
+make sanitize # rebuild and run the suite under ASan + UBSan
+```
+
+The suite stands up a real HTTP server on a loopback port and points the
+client at it through `base_url`. Nothing is mocked, and no test touches the
+network.
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).