diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c76d3d --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# cepis + +D client for the [public CEP API at cep.is](https://cep.is). Official Correios +data (DNE), refreshed every two weeks. + +```d +import cepis; + +auto address = lookup("01001-000"); +writeln(address.logradouro, " ", address.localidade, " ", address.uf); +// Praça da Sé São Paulo SP +``` + +## Installation + +```sh +dub add cepis +``` + +Or in `dub.json`: + +```json +"dependencies": { + "cepis": "~>0.1.0" +} +``` + +No dub dependencies: HTTP comes from `std.net.curl`, JSON from `std.json`, CSV +from `std.csv`. `std.net.curl` needs **libcurl at runtime** (`libcurl.so.4` on +Linux, `libcurl.dll` on Windows) and `libcurl` at link time — most systems +already ship it; on Debian and derivatives that is `libcurl4-openssl-dev`. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `Address`: + +```d +auto a = lookup("01310-100"); + +a.logradouro; // "Avenida Paulista" +a.localidade; // "São Paulo" +a.uf; // "SP" +a.ibge; // "3550308" +a.cep; // "01310100" (always unmasked) +a.formattedCep; // "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings. +`a.raw` holds the response exactly as it arrived, so a field the API grows +before this library is updated is not lost. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is null: + +```d +if (!a.geo.isNull) +{ + auto geo = a.geo.get; + writeln(geo.lat, " ", geo.lon); // -23.567202 -46.649659 + writeln(geo.raio_m); // 1084 + writeln(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 | + +`geo.isPrecise` is the shorthand for `precisao == "logradouro"`: + +```d +auto cost = !a.geo.isNull && a.geo.get.isPrecise + ? shippingFrom(a.geo.get.lat, a.geo.get.lon) + : shippingFromMunicipality(a.ibge); +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```d +auto ceps = ["01001000", "99999999", "20040020"]; + +foreach (index, address; lookupMany(ceps)) + writeln(ceps[index], ": ", address.isNull ? "not found" : address.get.localidade); +``` + +The returned array 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 does not throw. + +Arrays 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 `lookupMany` has a null `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: + +```d +auto h = 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 is not an error — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. It keeps no +state between calls and is safe to copy and share across threads: + +```d +ClientConfig config; +config.baseUrl = "https://cep.is"; // point at another instance +config.timeout = dur!"seconds"(10); // per socket operation +config.maxAttempts = 3; // counting the first; 1 disables retrying +config.backoffBase = dur!"msecs"(500); // base of the exponential wait +config.backoffMax = dur!"seconds"(8); // cap on each wait +config.totalTimeout = dur!"seconds"(30); // cap on the whole call +config.userAgent = "my-app/1.0"; + +auto client = Client(config); +auto address = client.lookup("01001000"); +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +The module-level functions (`lookup`, `lookupMany`, `health`) share one default +client per thread, built on first use. Need a different configuration? Build +your own. + +### Pointing at a local instance + +`baseUrl` is the only setting that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```d +ClientConfig config; +config.baseUrl = "http://localhost:8080"; + +auto client = Client(config); +``` + +## Errors + +Everything derives from `CepisException`, so one `catch` handles any failure. +Below that, the split is by **the decision you have to make**, not by the HTTP +status that caused it: + +| exception | when | does retrying help? | +| --- | --- | --- | +| `InvalidCepException` | not 8 digits | no | +| `CepNotFoundException` | well-formed, not in the database | no | +| `RateLimitedException` | 429; carries `retryAfter` | yes, later | +| `ServerException` | 5xx; carries `status` | yes, later | +| `NetworkException` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponseException` | unintelligible response | probably not | + +```d +try + render(lookup(cepFromForm)); +catch (CepNotFoundException) + askForAnotherCep(); +catch (CepisException error) +{ + if (error.retriable) + reschedule(); + else + stderr.writeln(error.msg); +} +``` + +`InvalidResponseException` 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 + +Everything runs through `make`, which shells out to `dub`: + +```sh +make # list the targets +make test # dub test +make lint # dscanner + a dfmt check, touches nothing +make format # apply dfmt (writes) +make build # release build +make check # lint, then test, then build +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |