summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:28:10 -0300
commitb71faa6e95a0736f1ff171e006b171467b65db90 (patch)
tree7580c3ab515e8f50726dc68dbc240995ff4a1a4c /README.md
downloadcpp-b71faa6e95a0736f1ff171e006b171467b65db90.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md238
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).