diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:28:10 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:28:10 -0300 |
| commit | b71faa6e95a0736f1ff171e006b171467b65db90 (patch) | |
| tree | 7580c3ab515e8f50726dc68dbc240995ff4a1a4c /README.md | |
| download | cpp-b71faa6e95a0736f1ff171e006b171467b65db90.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b85199 --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# cepis + +C++ client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```cpp +#include <cepis/cepis.hpp> +#include <iostream> + +int main() { + const cepis::Address a = cepis::lookup("01001-000"); + std::cout << a.logradouro << ' ' << a.localidade << ' ' << a.uf << '\n'; + // Praça da Sé São Paulo SP +} +``` + +Requires C++17 and libcurl. libcurl is the one dependency because the C++ +standard library has no HTTP client and libcurl is already present on +essentially every system this will run on, TLS and certificate store included; +JSON and CSV are parsed in-tree, since the payloads are flat and a +general-purpose parser would be the larger dependency of the two. + +## Installation + +```sh +git clone https://git.cep.is/cpp.git cepis-cpp +cd cepis-cpp +make build # build/libcepis.a and build/cepis-lookup +``` + +With CMake, either add the directory to your project or install it: + +```cmake +add_subdirectory(cepis-cpp) +target_link_libraries(your_target PRIVATE cepis::cepis) +``` + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +cmake --install build --prefix /usr/local +``` + +Link manually with `-lcepis -lcurl`. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `Address`: + +```cpp +const cepis::Address a = cepis::lookup("01310-100"); + +a.logradouro; // "Avenida Paulista" +a.localidade; // "São Paulo" +a.uf; // "SP" +a.ibge; // "3550308" +a.cep; // "01310100" (always unmasked) +a.formatted_cep(); // "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never absent. `a.raw` keeps the response as it arrived, so a field the API +grows before this library is updated is still reachable. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is empty: + +```cpp +if (a.geo) { + a.geo->lat; // -23.567202 + a.geo->lon; // -46.649659 + a.geo->raio_m; // 1084 + 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 | + +`a.geo->is_precise()` is the shorthand for `precisao == "logradouro"`: + +```cpp +const double cost = a.geo && a.geo->is_precise() + ? shipping_from(a.geo->lat, a.geo->lon) + : shipping_from_municipality(a.ibge); +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs +per request: + +```cpp +const std::vector<std::string> ceps = {"01001000", "99999999", "20040020"}; +const std::vector<std::optional<cepis::Address>> result = cepis::lookup_many(ceps); + +for (std::size_t i = 0; i < ceps.size(); ++i) { + if (result[i]) { + std::cout << ceps[i] << ": " << result[i]->localidade << '\n'; + } else { + std::cout << ceps[i] << ": not found\n"; + } +} +``` + +The returned vector has **the same size and the same order** as the input. A +CEP that does not exist, or is malformed, is empty at its position — it does +not vanish, and it does not throw. + +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 `lookup_many` has an empty +> `geo`, including for CEPs that do have one. Need `geo`? Use `lookup`, one at +> a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version +of the database** is live: + +```cpp +const cepis::Health h = cepis::health(); + +h.ok(); // true +h.dne; // "26071" identifier of the fortnightly database +h.ceps; // 1596168 +h.base_gerada_em; // "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded does not throw — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. Its +configuration is fixed at construction and it keeps no state between calls, so +a `const Client&` is safe to share across threads: + +```cpp +cepis::Config config; +config.base_url = "https://cep.is"; // another instance +config.timeout = std::chrono::seconds{10}; // cap on one attempt +config.max_attempts = 3; // 1 disables retrying +config.backoff_base = std::chrono::milliseconds{500}; // exponential base +config.backoff_max = std::chrono::seconds{8}; // cap on each wait +config.total_timeout = std::chrono::seconds{15}; // cap on the whole call + +const cepis::Client client(config); +const cepis::Address a = client.lookup("01001000"); +``` + +`timeout` caps one attempt — connection plus transfer — not each socket +operation, because that is how libcurl works. `total_timeout` is the cap on +the whole call, retries and waits included. + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. Each field is documented in +`include/cepis/client.hpp`. + +The free functions (`cepis::lookup`, etc.) share one process-wide client. Need +a different configuration? Build your own. + +### Pointing at a local instance + +`base_url` is the only parameter that usually changes between environments. +The API repo brings a local instance up with `just up`: + +```cpp +cepis::Config config; +config.base_url = "http://localhost:8080"; +``` + +From the command line, to confirm quickly that it answers: + +```sh +build/cepis-lookup --base-url http://localhost:8080 01001000 +``` + +## Errors + +Everything derives from `cepis::Error`, which derives from +`std::runtime_error`. Below that, the split is by **the decision you have to +make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| --- | --- | --- | +| `InvalidCep` | not 8 digits | no | +| `CepNotFound` | well-formed, not in the database | no | +| `RateLimited` | 429; exposes `retry_after()` | yes, later | +| `ServerError` | 5xx; exposes `status()` | yes, later | +| `NetworkError` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponse` | unintelligible response | probably not | + +```cpp +try { + const cepis::Address a = cepis::lookup(cep_from_form); +} catch (const cepis::CepNotFound&) { + // ask the user for another CEP +} catch (const cepis::Error&) { + // transient failure: reschedule +} +``` + +`InvalidResponse` 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. + +`Client`'s constructor throws `std::invalid_argument` for a configuration that +cannot work, such as `max_attempts = 0`. + +## Development + +Everything runs through `make`: + +```sh +make # list the targets +make test # build and run the suite +make lint # clang-format --dry-run + clang-tidy, touches nothing +make format # apply clang-format +make build # libcepis.a and the example CLI into build/ +make check # lint, then test, then build +``` + +The suite stands up a real HTTP server on a loopback port and points +`base_url` at it, so no test touches the network. Override the toolchain with +e.g. `make test CXX=clang++ STD=c++20`. + +## Licence + +ISC. See [LICENSE](LICENSE). |