diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 273 |
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). |